Skip to content

Commit

Permalink
feat(store): improve state types
Browse files Browse the repository at this point in the history
  • Loading branch information
DawidWraga committed May 24, 2024
1 parent 8ab6a4e commit 52093ef
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 3 deletions.
5 changes: 4 additions & 1 deletion packages/store/src/create-state-methods/state.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ export type State<TStateValue extends StateValue> = (TStateValue extends object
callback: (value: TStateValue, prevValue: TStateValue) => void,
options?: OnChangeOptions<TStateValue>
) => 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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export function createStoreContext<TCreator extends StoreApi<any, any> | AnyFn>(

type StoreParams = TCreator extends AnyFn
? Parameters<TCreator>[0]
: TCreator extends StoreApi<infer TState, infer TExtensions>
: // we must infer the TState AND TExtensions here for the ts complier to work correctly at build time
TCreator extends StoreApi<infer TState, infer TExtensions>
? { initialState?: Partial<TState> }
: never;

Expand Down
2 changes: 1 addition & 1 deletion packages/store/src/create-store/create-zustand-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,5 @@ export function state<TState extends StateValue>(
return createMethodsProxy({
zustandStore: zustandStore,
storeName: defWithDefaults.name,
}) as unknown as State<TState>;
}) as State<TState>;
}
88 changes: 88 additions & 0 deletions packages/store/test/v2/simplifying-complex-types.md
Original file line number Diff line number Diff line change
@@ -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<T>` type and a `createState` function:

```typescript
export type State<TStateValue extends StateValue> = {
// ...
get: <
TSelector extends (state: TStateValue) => unknown = (
state: TStateValue
) => TStateValue,
>(
selector?: TSelector
) => TSelector extends (state: TStateValue) => infer TReturnType
? TReturnType
: TStateValue;
// ...
};

function createState<T extends StateValue>(initialValue: T): State<T> {
// ...
}
```

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: <TSelector extends (state: number) => unknown = (state: number) => number>(selector?: TSelector | undefined) => TSelector extends (state: number) => infer TReturnType ? TReturnType : number;
set: (newValueOrFn: number | ((prev: number) => number)) => void;
use: <TSelector extends (state: number) => 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> = 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<TStateValue extends StateValue> = {
// ...
} & Simplify<{}>;
```

By intersecting the `State<T>` 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<T extends StateValue>(initialValue: T): State<T> {
// ...
}

const numberStore = createState(0);
// Hovering over `numberStore` will display `State<number>` instead of the expanded type
```

When using the `createState` function with the modified `State<T>` type, VS Code will display the simplified type name `State<number>` 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!
54 changes: 54 additions & 0 deletions packages/store/test/v2/store-types.test.ts
Original file line number Diff line number Diff line change
@@ -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<TStateValue> type', () => {
const myStore = store(0);
expectTypeOf(myStore).toEqualTypeOf<StoreApi<number>>();
//EXPECTED = const myStore: StoreApi<number>
//RECIEVED = const myStore: StoreApi<number>
// correctly inferred the type
});
it('state should have State<TStateValue> type', () => {
const myState = state(0);
expectTypeOf(myState).toEqualTypeOf<State<number>>();

// EXPECTED = const myState: State<number>
// RECIEVED = const myState: State<number>
// 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<number>;
// the type is expanded to something like this:
/**
const myState: {
get: <TSelector extends (state: number) => unknown = (state: number) => number>(selector?: TSelector | undefined) => TSelector extends (state: number) => infer TReturnType ? TReturnType : number;
set: (newValueOrFn: number | ((prev: number) => number)) => void;
use: <TSelector extends (state: number) => 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<T extends StateValue>() {
return null as unknown as State<T>
}

// and then use the function to get the type, like this:
const myStore = getStore<number>();

// then you get the correct type:
/**
const myStore: State<number>
*/

// this is a workaround to get the correct type. It's not ideal, but it gets the job done.

0 comments on commit 52093ef

Please sign in to comment.