mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
docs(typescript): Rewrite the inference explanation (#1252)
* docs(typescript): Rewrite the inference explanation * run prettier * fix grammar * run prettier * typo * grammar, punctuation * more edits * replacing ellipses with colon for consistency * even more edits * edit summary Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com> * fix hypothetical type Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
This commit is contained in:
parent
445e147478
commit
b8ec61dc53
@ -49,15 +49,26 @@ Here, if you look at the type of `f` in `create`, i.e. `(get: () => T) => T`, it
|
||||
So, as long as the generic to be inferred is invariant (i.e. both covariant and contravariant), TypeScript will be unable to infer it. Another simple example would be this:
|
||||
|
||||
```ts
|
||||
declare const createFoo: <T>(f: (t: T) => T) => T
|
||||
const createFoo = {} as <T>(f: (t: T) => T) => T
|
||||
const x = createFoo((_) => 'hello')
|
||||
```
|
||||
|
||||
Here again, `x` is `unknown` instead of `string`.
|
||||
|
||||
One could argue that it is impossible to write an implementation for `createFoo`, and that would be true. But then it is also impossible to write Zustand's `create` as a type. However, Zustand exists. So what do we mean by that?
|
||||
<details>
|
||||
<summary>More about the inference (just for the people curious and interested in TypeScript)</summary><br>
|
||||
|
||||
In some sense this inference failure is not a problem because a value of type `<T>(f: (t: T) => T) => T` cannot be written. That is to say you can't write the real runtime implementation of `createFoo`. Let's try it:
|
||||
|
||||
The thing is Zustand is lying in its type. The simplest way to prove it is by showing unsoundness. Consider this example:
|
||||
```js
|
||||
const createFoo = (f) => f(/* ? */)
|
||||
```
|
||||
|
||||
`createFoo` needs to return the return value of `f`. And to do that we first have to call `f`. And to call it we have to pass a value of type `T`. And to pass a value of type `T` we first have to produce it. But how can we produce a value of type `T` when we don't even know what `T` is? The only way to produce a value of type `T` is to call `f`, but then to call `f` itself we need a value of type `T`. So you see it's impossible to actually write `createFoo`.
|
||||
|
||||
So what we're saying is, the inference failure in case of `createFoo` is not really a problem because it's impossible to implement `createFoo`. But what about the inference failure in case of `create`? That also is not really a problem because it's impossible to implement `create` too. Wait a minute, if it's impossible to implement `create` then how does Zustand implement it? The answer is, it doesn't.
|
||||
|
||||
Zustand lies that it implemented `create`'s type, it implemented only the most part of it. Here's a simple proof by showing unsoundness. Consider the following code:
|
||||
|
||||
```ts
|
||||
import create from 'zustand/vanilla'
|
||||
@ -67,9 +78,13 @@ const useBoundStore = create<{ foo: number }>()((_, get) => ({
|
||||
}))
|
||||
```
|
||||
|
||||
This code compiles. However, you will get an exception when you run it: "Uncaught TypeError: Cannot read properties of undefined (reading 'foo')". This is because `get` would return `undefined` before the initial state is created (hence you shouldn't call `get` when creating the initial state). But the types tell that `get` is of type `() => { foo: number }`, which is exactly the lie I was taking about. The type of `get` is eventually that, but first it is `() => undefined`.
|
||||
This code compiles. But if we run it, we'll get an exception: "Uncaught TypeError: Cannot read properties of undefined (reading 'foo')". This is because `get` would return `undefined` before the initial state is created (hence you shouldn't call `get` when creating the initial state). The types promise that `get` will never return `undefined` but it does initially, which means Zustand failed to implement it.
|
||||
|
||||
Long story short, Zustand has a bit type-theoretically wild runtime behavior, which can't be typed in a sound and inferrable way with the currently available TypeScript features. To workaround the lack of inferrence, we provide the state type via a type parameter, and the tiny bit of unsoundness isn't a problem.
|
||||
And of course Zustand failed because it's impossible to implement `create` the way types promise (in the same way it's impossible to implement `createFoo`). In other words we don't have a type to express the actual `create` we have implemented. We can't type `get` as `() => T | undefined` because it would cause inconveince and it still won't be correct as `get` is indeed `() => T` eventually, just if called synchronously it would be `() => undefined`. What we need is some kind of TypeScript feature that allows us to type `get` as `(() => T) & WhenSync<() => undefined>`, which of course is extremly far-fetched.
|
||||
|
||||
So we have two problems: lack of inference and unsoundness. Lack of inference can be solved if TypeScript can improves its inference for invariants. And unsoundness can be solved if TypeScript introduces something like `WhenSync`. To work around lack of inference we manually annotate the state type. And we can't work around unsoundness, but it's not a big deal because it's not much, calling `get` synchronously anyway doesn't make sense.
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user