diff --git a/docs/typescript.md b/docs/typescript.md new file mode 100644 index 0000000000..af599af9ff --- /dev/null +++ b/docs/typescript.md @@ -0,0 +1,382 @@ +# TypeScript Guide + +## Basic usage + +When using TypeScript you just have to make a tiny change that instead of writing `create(...)` you'll have to write `create()(...)` where `T` would be type of the state so as to annotate it. Example... + +```ts +import create from "zustand" + +interface BearState { + bears: number + increase: (by: number) => void +} + +const useStore = create()((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +})) +``` + +
+ Why can't we just simply infer the type from initial state? + + **TLDR**: Because state generic `T` is invariant. + + Consider this minimal version `create`... + + ```ts + declare const create: (f: (get: () => T) => T) => T + + const x = create((get) => ({ + foo: 0, + bar: () => get() + })) + // `x` is inferred as `unknown` instead of + // interface X { + // foo: number, + // bar: () => X + // } + ``` + + Here if you look at the type of `f` in `create` ie `(get: () => T) => T` it "gives" `T` as it returns `T` but then it also "takes" `T` via `get` so where does `T` come from TypeScript thinks... It's a like that chicken or egg problem. At the end TypeScript gives up and infers `T` as `unknown`. + + So as long as the generic to be inferred is invariant TypeScript won't be able to infer it. Another simple example would be this... + + ```ts + declare const createFoo: (f: (t: T) => T) => T + const x = createFoo(_ => "hello") + ``` + + Here again `x` is `unknown` instead of `string`. + + Now one can argue it's impossible to write an implementation for `createFoo`, and that's true. But then it's also impossible to write Zustand's `create`... Wait but Zustand exists? So what do I mean by that? + + The thing is Zustand is lying in it's type, the simplest way to prove it by showing unsoundness. Consider this example... + + ```ts + import create from "zustand/vanilla" + + const useStore = create<{ foo: number }>()((_, get) => ({ + foo: get().foo, + })) + ``` + + This code compiles, but guess what happens when you run it? You'll get an exception "Uncaught TypeError: Cannot read properties of undefined (reading 'foo') because after all `get` would return `undefined` before the initial state is created (hence kids don't call `get` when creating the initial state). But the types tell that get is `() => { foo: number }` which is exactly the lie I was taking about, `get` is that eventually but first it's `() => undefined`. + + Okay we're quite deep in the rabbit hole haha, long story short zustand has a bit crazy runtime behavior that can't be typed in a sound way and inferrable way. We could make it inferrable with the right TypeScript features that don't exist today. And hey that tiny bit of unsoundness is not a problem. +
+ +
+ Why that currying `()(...)`? + + **TLDR**: It's a workaround for [microsoft/TypeScript#10571](https://github.com/microsoft/TypeScript/issues/10571). + + Imagine you have a scenario like this... + + ```ts + declare const withError: (p: Promise) => + Promise<[error: undefined, value: T] | [error: E, value: undefined]> + declare const doSomething: () => Promise + + const main = async () => { + let [error, value] = await withError(doSomething()) + } + ``` + + Here `T` is inferred as `string` and `E` is inferred as `unknown`. Now for some reason you want to annotate `E` as `Foo` because you're certain what shape of error `doSomething()` would throw. But too bad you can't do that, you can either pass all generics or none. So now along with annotating `E` as `Foo` you'll also have to annotate `T` as `string` which gets inferred anyway. So what to do? What you do is make a curried version of `withError` that does nothing in runtime, it's purpose is to just allow you annotate `E`... + + ```ts + declare const withError: { + (): (p: Promise) => + Promise<[error: undefined, value: T] | [error: E, value: undefined]> + (p: Promise): + Promise<[error: undefined, value: T] | [error: E, value: undefined]> + } + declare const doSomething: () => Promise + interface Foo { bar: string } + + const main = async () => { + let [error, value] = await withError()(doSomething()) + } + ``` + + And now `T` gets inferred and you get to annotate `E` too. Zustand has the same use case we want to annotate the state (the first type parameter) but allow the rest type parameters to get inferred. +
+ +Alternatively you can also use `combine` which infers the state instead of you having to type it... + +```ts +import create from "zustand" +import { combine } from "zustand/middleware" + +const useStore = create(combine({ bears: 0 }, (set) => ({ + increase: (by: number) => set((state) => ({ bears: state.bears + by })), +})) +``` + +
+ But be a little careful... + + We achieve the inference by lying a little in the types of `set`, `get` and `store` that you receive as parameters. The lie is that they're typed in a way as if the state is the first parameter only when in fact the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. So for example `get` from the second parameter has type `() => { bears: number }` and that's a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useStore` still has the correct type, ie for example `useStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. + + It's not a lie lie because `{ bears: number }` is still a subtype `{ bears: number, increase: (by: number) => void }`, so in most cases there won't be a problem. Just you have to be careful while using replace. For eg `set({ bears: 0 }, true)` would compile but will be unsound as it'll delete the `increase` function. (If you set from "outside" ie `useStore.setState({ bears: 0 }, true)` then it won't compile because the "outside" store knows that `increase` is missing.) Another instance where you should be careful you're doing `Object.keys`, `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]` (the return type of `get` can make you fall for this). + + So `combine` trades-off a little type-safety for the convience of not having to write a type for state. Hence you should use `combine` accordingly, usually it's not a big deal and it's okay to use it. +
+ +## Using middlewares + +You don't have to do anything special to use middlewares in TypeScript. + +```ts +import create from "zustand" +import { devtools, persist } from "zustand/middleware" + +interface BearState { + bears: number + increase: (by: number) => void +} + +const useStore = create()(devtools(persist((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +})))) +``` + +Just make sure you're using them immediately inside `create` so as to make the contextual inference work. Doing something even remotely fancy like the following `myMiddlewares` would require more advanced types. + +```ts +import create from "zustand" +import { devtools, persist } from "zustand/middleware" + +const myMiddlewares = f => devtools(persist(f)) + +interface BearState { + bears: number + increase: (by: number) => void +} + +const useStore = create()(myMiddlewares((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +}))) +``` + +## Authoring middlewares and advanced usage + +Imagine you had to write this hypothetical middleware... + +```js +import create from "zustand" + +const foo = (f, bar) => (set, get, store) => { + store.foo = bar + return f(set, get, store); +} + +const useStore = create(foo(() => ({ bears: 0 }), "hello")) +console.log(store.foo.toUpperCase()) +``` + +Yes, if you didn't know Zustand middlewares do and are allowed to mutate the store. But how could we possibly encode the mutation on the type-level? That is to say how could do we type `foo` so that this code compiles? + +For an usual statically typed language this is impossible, but thanks to TypeScript, Zustand has something called an "higher kinded mutator" that makes this possible. If you're dealing with complex type problems like typing a middleware or using the `StateCreator` type, then you'll have to understand this implementation detail, for that check out [#710](https://github.com/pmndrs/zustand/issues/710). + +If you're eager to know what the answer is to this particular problem then it's [here](#middleware-that-changes-the-store-type). + +## Common recipes + +### Middleware that does not change the store type + +```ts +import create, { State, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi } from "zustand" + +type Logger = + < T extends State + , Mps extends [StoreMutatorIdentifier, unknown][] = [] + , Mcs extends [StoreMutatorIdentifier, unknown][] = [] + > + ( f: StateCreator + , name?: string + ) => + StateCreator + +type LoggerImpl = + + ( f: PopArgument> + , name?: string + ) => + PopArgument> + +const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => { + type T = ReturnType + const loggedSet: typeof set = (...a) => { + set(...a) + console.log(...(name ? [`${name}:`] : []), get()) + } + store.setState = loggedState + + return f(loggedSet, get, store) +} + +export const logger = loggerImpl as unknown as Foo + +type PopArgument unknown> = + T extends (...a: [...infer A, infer _]) => infer R + ? (...a: A) => R + : never + +// --- + +const useStore = create()(logger((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +}), "bear-store")) +``` + +### Middleware that changes the store type + +```js +import create, { State, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi } from "zustand" + +type Foo = + < T extends State + , A + , Mps extends [StoreMutatorIdentifier, unknown][] = [] + , Mcs extends [StoreMutatorIdentifier, unknown][] = [] + > + ( f: StateCreator + , bar: A + ) => + StateCreator + +declare module 'zustand' { + interface StoreMutators { + foo: Write { foo: A }> + } +} + +type FooImpl = + + ( f: PopArgument> + , bar: A + ) => PopArgument> + +const fooImpl: FooImpl = (f, bar) => (set, get, _store) => { + type T = ReturnType + type A = typeof bar + + const store = _store as Mutate, [['foo', A]]> + store.foo = bar + return f(set, get, _store) +} + +export const foo = fooImpl as unknown as Foo + +type PopArgument unknown> = + T extends (...a: [...infer A, infer _]) => infer R + ? (...a: A) => R + : never + +type Write = + Omit & U + +type Cast = + T extends U ? T : U; + +// --- + +const useStore = create(foo(() => ({ bears: 0 }), "hello")) +console.log(store.foo.toUpperCase()) +``` + +### `create` without curried workaround + +The recommended way to use `create` is using the curried workaround ie `create()(...)` because this enabled you to infer the store type. But for some reason if you don't want to use the workaround then you can pass the type parameters like the following. Note that in some cases this acts as an assertion instead of annotation, so it's not recommended. + +```ts +import create, { State, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi } from "zustand" + +interface BearState { + bears: number + increase: (by: number) => void +} + +const useStore = create< + BearState, + [ + ['zustand/persist', BearState], + ['zustand/devtools', never] + ] +>(devtools(persist((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +}))) +``` + +### Independent slices pattern + +```ts +import create, { State, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi } from "zustand" + +interface BearSlice { + bears: number + addBear: () => void +} +const createBearSlice: StateCreator = (set) => ({ + bears: 0, + addBear: () => set((state) => ({ bears: state.bears + 1 })) +}) + +interface FishSlice { + fishes: number + addFish: () => void +} +const createFishSlice: StateCreator = (set) => ({ + fishes: 0, + addFish: () => set((state) => ({ fishes: state.fishes + 1 })) +}) + +const useStore = create()((...a) => ({ + ...createBearSlice(...a), + ...createFishSlice(...a) +})) +``` + +If you have some middlewares then replace `StateCreator` with `StateCreator`. Eg if you're using `devtools` then it'll be `StateCreator`. + +Also you can even write `StateCreator` instead of `StateCreator` as the second and third parameter have `[]` as their default value. + +### Interdependent slices pattern + +```ts +import create, { State, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi } from "zustand" + +interface BearSlice { + bears: number + addBear: () => void + eatFish: () => void +} +const createBearSlice: StateCreator = (set) => ({ + bears: 0, + addBear: () => set((state) => ({ bears: state.bears + 1 })), + eatFish: () => set((state) => ({ fishes: state.fishes - 1 })) +}) + +interface FishSlice { + fishes: number + addFish: () => void +} +const createFishSlice: StateCreator = (set) => ({ + fishes: 0, + addFish: () => set((state) => ({ fishes: state.fishes + 1 })) +}) + +const useStore = create()((...a) => ({ + ...createBearSlice(...a), + ...createFishSlice(...a) +})) +``` + +If you have some middlewares then replace `StateCreator` with `StateCreator`. Eg if you're using `devtools` then it'll be `StateCreator`. diff --git a/docs/v4-migration.md b/docs/v4-migration.md new file mode 100644 index 0000000000..be8c03aac9 --- /dev/null +++ b/docs/v4-migration.md @@ -0,0 +1,205 @@ +# v4 Migrations + +If you're not using the typed version (either via TypeScript or via JSDoc) then there are no breaking changes for you and hence no migration is needed either. + +Also it's recommended to first read the new [TypeScript Guide](https://github.com/pmndrs/zustand/blob/main/docs/typescript.md), it'll be easier to understand the migration. + +In addition to this migration guide you can also check the diff of the test files in the repo from v3 to v4. + +## `create` (from `zustand` and `zustand/vanilla`) + +### Change + +```diff +- create: +- < State +- , StoreSetState = StoreApi["set"] +- , StoreGetState = StoreApi["get"] +- , Store = StoreApi +- > +- (f: ...) => ... ++ create: ++ { (): (f: ...) => ... ++ , (f: ...) => ... ++ } +``` + +### Migration + +If you're not passing any type parameters to `create` then there is no migration needed. If you're using a "leaf" middleware like `combine` or `redux` then remove all type parameters from `create`. Else replace `create(...)` with `create()(...)`. + +## `StateCreator` (from `zustand` and `zustand/vanilla`) + +### Change + +```diff +- type StateCreator +- < State +- , StoreSetState = StoreApi["set"] +- , StoreGetState = StoreApi["get"] +- , Store = StoreApi +- > = +- ... ++ type StateCreator ++ < State ++ , InMutators extends [StoreMutatorIdentifier, unknown][] = [] ++ , OutMutators extends [StoreMutatorIdentifier, unknown][] = [] ++ , Return = State ++ > = ++ ... +``` + +### Migration + +If you're using `StateCreator` you're likely authoring a middleware or using the "slices" pattern, for that check the TypeScript Guide's ["Authoring middlewares and advanced usage"](https://github.com/pmndrs/zustand/blob/main/docs/typescript.md#authoring-middlewares-and-advanced-usage) and ["Common recipes"](https://github.com/pmndrs/zustand/blob/main/docs/typescript.md#authoring-middlewares-and-advanced-usage) sections. + +## `PartialState` (from `zustand` and `zustand/vanilla`) + +### Change + +```diff +- type PartialState +- < T extends State +- , K1 extends keyof T = keyof T +- , K2 extends keyof T = K1 +- , K3 extends keyof T = K2 +- , K4 extends keyof T = K3 +- > = +- | (Pick | Pick | Pick | Pick | T) +- | ((state: T) => Pick | Pick | Pick | Pick | T) ++ type PartialState = ++ | Partial ++ | ((state: T) => Partial) +``` + +### Migration + +Replace `PartialState` with `PartialState` and preferably turn on [`--exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes)/ + +We're no longer using the trick to disallow `{ foo: undefined }` to be assigned to `Partial<{ foo: string }>` instead now we're relying on the users to turn on `--exactOptionalPropertyTypes`. + +## `useStore` (from `zustand` and `zustand/react`) + +### Change + +```diff +- useStore: +- { (store: StoreApi): State +- , +- ( store: StoreApi +- , selector: StateSelector, +- , equals?: EqualityChecker +- ): StateSlice +- } ++ useStore: ++ > ++ ( store: Store ++ , selector?: StateSelector, ++ , equals?: EqualityChecker ++ ) ++ => StateSlice +``` + +### Migration + +If you're not passing any type parameters to `useStore` then there is no migration needed. If you are then it's recommended to remove them, or pass the store type instead of the state type as the first parameter. + +## `UseBoundStore` (from `zustand` and `zustand/react`) + +### Change + +```diff +- type UseBoundStore< +- State, +- Store = StoreApi +- > = +- & { (): T +- , +- ( selector: StateSelector +- , equals?: EqualityChecker +- ): U +- } +- & Store ++ type UseBoundStore = ++ & (> ++ ( selector?: (state: ExtractState) => StateSlice ++ , equals?: (a: StateSlice, b: StateSlice) => boolean ++ ) => StateSlice ++ ) ++ & S +``` + +### Migration + +Replace `UseBoundStore` with `UseBoundStore>` and `UseBoundStore` with `UseBoundStore` + +## `UseContextStore` (from `zustand/context`) + +### Change + +```diff +- type UseContextStore +``` + +### Migration + +Use `typeof MyContext.useStore` instead + +## `createContext` (from `zustand/context`) + +### Change + +```diff + createContext: +- >() => ... ++ () => ... +``` + +### Migration + +Replace `createContext()` with `createContext>()` and `createContext()` with `createContext()`. + +## `combine`, `devtools`, `persist`, `subscribeWithSelector` (from `zustand/middleware`) + +### Change + +```diff +- combine: +- (...) => ... ++ combine: ++ (...) => ... + +- devtools: +- (...) => ... ++ devtools: ++ (...) => ... + +- persist: +- (...) => ... ++ persist: ++ (...) => ... + +- subscribeWithSelector: +- (...) => ... ++ subscribeWithSelector: ++ (...) => ... +``` + +### Migration + +If you're not passing any type parameters then there is no migration needed. If you're passing any type parameters, remove them as are inferred. + +## `redux` (from `zustand/middleware`) + +### Change + +```diff +- redux: +- (...) => ... ++ redux: ++ (...) => ... +``` + +### Migration + +If you're not passing any type parameters then there is no migration needed. If you're passing type parameters them remove them and annotate the second (action) parameter. That is replace `redux((state, action) => ..., ...)` with `redux((state, action: A) => ..., ...)`. diff --git a/readme.md b/readme.md index cfd7a22d0a..7c8cf4cefa 100644 --- a/readme.md +++ b/readme.md @@ -196,30 +196,6 @@ const unsub4 = useStore.subscribe(state => [state.paw, state.fur], console.log, const unsub5 = useStore.subscribe(state => state.paw, console.log, { fireImmediately: true }) ``` -
-How to type store with `subscribeWithSelector` in TypeScript - -```ts -import create, { Mutate, GetState, SetState, StoreApi } from 'zustand' -import { subscribeWithSelector } from 'zustand/middleware' - -type BearState = { - paw: boolean - snout: boolean - fur: boolean -} -const useStore = create< - BearState, - SetState, - GetState, - Mutate, [["zustand/subscribeWithSelector", never]]> ->(subscribeWithSelector(() => ({ paw: true, snout: true, fur: true }))) -``` - -For more complex typing with multiple middlewares, -Please refer [middlewareTypes.test.tsx](./tests/middlewareTypes.test.tsx). -
- ## Using zustand without React Zustands core can be imported and used without the React dependency. The only difference is that the create function does not return a hook, but the api utilities. @@ -306,36 +282,6 @@ const useStore = create( ) ``` -
-How to pipe middlewares - -```js -import create from "zustand" -import produce from "immer" -import pipe from "ramda/es/pipe" - -/* log and immer functions from previous example */ -/* you can pipe as many middlewares as you want */ -const createStore = pipe(log, immer, create) - -const useStore = createStore(set => ({ - bears: 1, - increasePopulation: () => set(state => ({ bears: state.bears + 1 })) -})) - -export default useStore -``` - -For a TS example see the following [discussion](https://github.com/pmndrs/zustand/discussions/224#discussioncomment-118208) -
- -
-How to type immer middleware in TypeScript - -There is a reference implementation in [middlewareTypes.test.tsx](./tests/middlewareTypes.test.tsx) with some use cases. -You can use any simplified variant based on your requirement. -
- ## Persist middleware You can persist your store's data using any kind of storage. @@ -581,48 +527,32 @@ const Component = () => { >