From 52093efeb11a8ce121d8042b04b8886f8504ee37 Mon Sep 17 00:00:00 2001 From: DawidWraga Date: Fri, 24 May 2024 11:44:22 +0100 Subject: [PATCH] feat(store): improve state types --- .../src/create-state-methods/state.types.ts | 5 +- .../create-store-context.tsx | 3 +- .../src/create-store/create-zustand-store.tsx | 2 +- .../test/v2/simplifying-complex-types.md | 88 +++++++++++++++++++ packages/store/test/v2/store-types.test.ts | 54 ++++++++++++ 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 packages/store/test/v2/simplifying-complex-types.md create mode 100644 packages/store/test/v2/store-types.test.ts diff --git a/packages/store/src/create-state-methods/state.types.ts b/packages/store/src/create-state-methods/state.types.ts index 7b04439..6f88c16 100644 --- a/packages/store/src/create-state-methods/state.types.ts +++ b/packages/store/src/create-state-methods/state.types.ts @@ -69,7 +69,10 @@ export type State = (TStateValue extends object callback: (value: TStateValue, prevValue: TStateValue) => void, options?: OnChangeOptions ) => UnsubscribeFn; - }; + } & /** + * Using this & Simplify<{}> does not change the types in any way, it's just a weird work around that I found that changes how the types are displayed. Instead of showing the entire type on hover, it just shows the name of the type - making it much easier to read. + */ + Simplify<{}>; export type UnsubscribeFn = () => void; diff --git a/packages/store/src/create-store-context/create-store-context.tsx b/packages/store/src/create-store-context/create-store-context.tsx index 825ce83..4208e66 100644 --- a/packages/store/src/create-store-context/create-store-context.tsx +++ b/packages/store/src/create-store-context/create-store-context.tsx @@ -15,7 +15,8 @@ export function createStoreContext | AnyFn>( type StoreParams = TCreator extends AnyFn ? Parameters[0] - : TCreator extends StoreApi + : // we must infer the TState AND TExtensions here for the ts complier to work correctly at build time + TCreator extends StoreApi ? { initialState?: Partial } : never; diff --git a/packages/store/src/create-store/create-zustand-store.tsx b/packages/store/src/create-store/create-zustand-store.tsx index 4f09381..0c2c94b 100644 --- a/packages/store/src/create-store/create-zustand-store.tsx +++ b/packages/store/src/create-store/create-zustand-store.tsx @@ -117,5 +117,5 @@ export function state( return createMethodsProxy({ zustandStore: zustandStore, storeName: defWithDefaults.name, - }) as unknown as State; + }) as State; } diff --git a/packages/store/test/v2/simplifying-complex-types.md b/packages/store/test/v2/simplifying-complex-types.md new file mode 100644 index 0000000..52c887e --- /dev/null +++ b/packages/store/test/v2/simplifying-complex-types.md @@ -0,0 +1,88 @@ +Title: "Simplifying Complex Type Display in TypeScript and VS Code" + +Introduction: +When working with TypeScript and VS Code, you may encounter situations where complex types, especially those involving generics and conditional types, are displayed in a verbose and difficult-to-read manner when hovering over variables or types. This can make it challenging to quickly understand the type information and can clutter the IDE's interface. In this article, we'll explore a workaround that can help simplify the display of complex types in VS Code using TypeScript. + +The Problem: +Consider the following example where we have a `State` type and a `createState` function: + +```typescript +export type State = { + // ... + get: < + TSelector extends (state: TStateValue) => unknown = ( + state: TStateValue + ) => TStateValue, + >( + selector?: TSelector + ) => TSelector extends (state: TStateValue) => infer TReturnType + ? TReturnType + : TStateValue; + // ... +}; + +function createState(initialValue: T): State { + // ... +} +``` + +When using the `createState` function to create a store with a specific type, like `createState(0)`, VS Code expands the type and displays a verbose and complex type definition: + +```typescript +const numberStore = createState(0); +/* +numberStore: { + get: unknown = (state: number) => number>(selector?: TSelector | undefined) => TSelector extends (state: number) => infer TReturnType ? TReturnType : number; + set: (newValueOrFn: number | ((prev: number) => number)) => void; + use: unknown = (state: number) => number>(selector?: TSelector | undefined, equalityFn?: EqualityChecker<...> | undefined) => TSelector extends (state: number) => infer TReturnType ? TReturnType : number; + onChange: (callback: (value: number, prevValue: number) => void, options?: OnChangeOptions<...> | undefined) => UnsubscribeFn; +} +*/ +``` + +The Solution: +To simplify the display of complex types in VS Code, we can leverage TypeScript's intersection types and a clever type definition. Here's how it works: + +1. Define a `Simplify` utility type: + +```typescript +export type Simplify = T extends any[] | Date + ? T + : { + [K in keyof T]: T[K]; + } & {}; +``` + +This type expands objects to show all the key/value types. Given an empty object, it will resolve to an empty object. + +2. Intersect your complex type with `Simplify<{}>`: + +```typescript +export type State = { + // ... +} & Simplify<{}>; +``` + +By intersecting the `State` type with `Simplify<{}>`, we trigger a mechanism in VS Code that simplifies the displayed type when hovering over variables or types, without altering the actual type. + +3. Use the modified type in the `createState` function: + +```typescript +function createState(initialValue: T): State { + // ... +} + +const numberStore = createState(0); +// Hovering over `numberStore` will display `State` instead of the expanded type +``` + +When using the `createState` function with the modified `State` type, VS Code will display the simplified type name `State` instead of the verbose type definition when hovering over `numberStore`. + +Conclusion: +The workaround presented in this article can help simplify the display of complex types in VS Code when working with TypeScript. By intersecting your complex types with `Simplify<{}>`, you can trigger a mechanism in VS Code that simplifies the displayed type, making it more readable and easier to understand. + +However, it's important to note that this approach is a workaround and may not be suitable for all situations. It's still crucial to have well-defined and properly structured types in your codebase. The simplified type display is primarily aimed at improving readability and reducing clutter in the IDE. + +Remember, this technique only affects the display of types in VS Code and doesn't change the actual type definitions. It's a tool to enhance the developer experience when working with complex types. + +I hope this article helps you navigate and simplify the display of complex types in your TypeScript projects using VS Code. Happy coding! diff --git a/packages/store/test/v2/store-types.test.ts b/packages/store/test/v2/store-types.test.ts new file mode 100644 index 0000000..48260c1 --- /dev/null +++ b/packages/store/test/v2/store-types.test.ts @@ -0,0 +1,54 @@ +import { describe, expectTypeOf, it } from 'vitest'; +import { Simplify, State, state, StateValue, store, StoreApi } from '../../src'; + +describe('correct store types', () => { + it('store should have StoreApi type', () => { + const myStore = store(0); + expectTypeOf(myStore).toEqualTypeOf>(); + //EXPECTED = const myStore: StoreApi + //RECIEVED = const myStore: StoreApi + // correctly inferred the type + }); + it('state should have State type', () => { + const myState = state(0); + expectTypeOf(myState).toEqualTypeOf>(); + + // EXPECTED = const myState: State + // RECIEVED = const myState: State + // correctly inferred the type + }); +}); + +// notes on how I fixed it: + +// to fix it, I added & Simplify<{}> to the state type. +// adding just & {} didn't work +// maybe it's because there are two generics being resolved so the IDE just simplifies the type to the name of the type, instead of showing the entire type on hover. +// by making advantage of this mechanism, I think we can just add any type that doesn't change the type, like Simplify<{}> to make the type easier to read. + +// for some reason, when passing the generic directly to the staet function, like this: +type StoreApi2Number = State; +// the type is expanded to something like this: +/** + const myState: { + get: unknown = (state: number) => number>(selector?: TSelector | undefined) => TSelector extends (state: number) => infer TReturnType ? TReturnType : number; + set: (newValueOrFn: number | ((prev: number) => number)) => void; + use: unknown = (state: number) => number>(selector?: TSelector | undefined, equalityFn?: EqualityChecker<...> | undefined) => TSelector extends (state: number) => infer TReturnType ? TReturnType : number; + onChange: (callback: (value: number, prevValue: number) => void, options?: OnChangeOptions<...> | undefined) => UnsubscribeFn; + */ + +// however, if you cast the type as a return value of a function, like this: +function getStore() { + return null as unknown as State +} + +// and then use the function to get the type, like this: +const myStore = getStore(); + +// then you get the correct type: +/** + const myStore: State + */ + +// this is a workaround to get the correct type. It's not ideal, but it gets the job done. +