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:
Devansh Jethmalani 2022-09-03 05:18:06 +05:30 committed by GitHub
parent 445e147478
commit b8ec61dc53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -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>