-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(docs): Issue 1120 docs guides #1244
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit df820bf:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
other than those, it looks good.
docs/guides/typescript.md
Outdated
|
||
<br/> | ||
|
||
**TLDR**: Because state generic `T` is invariant. | ||
|
||
Consider this minimal version `create`... | ||
Consider this minimal version `create...` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not "create
:"?
docs/guides/typescript.md
Outdated
@@ -132,23 +132,23 @@ const useBearStore = create( | |||
``` | |||
|
|||
<details> | |||
<summary>But be a little careful...</summary> | |||
<summary>Be careful</summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you leave "little" or something? This describes very rare case and most people don't need to read. So, maybe "careful" is misleading. cc @devanshj
@dai-shi I've made changes based on your comments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the edit. I've left my two cents. Most of them are trying to preserve and improve the expression and intent as the original author. But of course I do understand I don't own the document haha so no pressure. Other than that...
- I like the punctuations, makes it more readable.
- I also like the more simplified phrasing at places.
- I personally don't like the use of non-contractions ie "do not" instead of "don't". Reading "do not" feels too passive to me. But I'm fine it others are okay with it.
- I also personally prefer "..." instead of ":" but again I'm fine as long as we're consistent.
cc @dai-shi
@@ -5,7 +5,7 @@ nav: 9 | |||
|
|||
## Basic usage | |||
|
|||
The difference when using TypeScript is instead of writing `create(...)`, you have to write `create<T>()(...)` where `T` would be type of the state so as to annotate it. Example... | |||
The difference when using TypeScript is that instead of typing `create(...)`, you have to type `create<T>()(...)`. `T` would have a type of the state to annotate it. For example: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of typing
create(...)
, you have to typecreate<T>()(...)
I'm not sure what do you mean by "typing" here. If by "typing" you mean TypeScript-typing then we're not only typing per se when we write the code create<T>()(...)
.
T
would have a type of the state to annotate it.
T
does not "have" the type of state, it "is" the type of state.
The difference when using TypeScript is that instead of typing `create(...)`, you have to type `create<T>()(...)`. `T` would have a type of the state to annotate it. For example: | |
The difference when using TypeScript is that instead of writing `create(...)`, you have to write `create<T>()(...)` where `T` is type of the state so as to annotate it. For example: |
If you want something simpler then the following would do too...
The difference when using TypeScript is that instead of typing `create(...)`, you have to type `create<T>()(...)`. `T` would have a type of the state to annotate it. For example: | |
The difference when using TypeScript is that instead of writing `create(...)`, you have to write `create<T>()(...)` where `T` is the type of the state. For example: |
@@ -44,20 +44,20 @@ const x = create((get) => ({ | |||
// } | |||
``` | |||
|
|||
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`. | |||
Here, if you look at the type of `f` in `create`, i.e. `(get: () => T) => T`, it returns `T`. However, it also "takes" `T` via `get`. Typescript wonders where `T` comes from, like that chicken or egg problem. At the end TypeScript, gives up and infers `T` as `unknown`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it returns
T
It's important to keep the original phrase with "gives" in quotes. Here "gives" implies covariance and "takes" implies contravariance. It's possible to "give" without returning, for example, in type F<T> = (g: (x: T) => void) => void
, F
is giving T
because when you write the implementation of F
you are the one that will pass x
(ie T
) to g
and hence "give"/"send" it outside.
Made some other punctuations corrections to my original copy to express my intention better.
Here, if you look at the type of `f` in `create`, i.e. `(get: () => T) => T`, it returns `T`. However, it also "takes" `T` via `get`. Typescript wonders where `T` comes from, like that chicken or egg problem. At the end TypeScript, gives up and infers `T` as `unknown`. | |
Here, if you look at the type of `f` in `create`, i.e. `(get: () => T) => T`, it "gives" `T` via returning (making it covariant), but it also "takes" `T` via `get` (making it contravariant). "So where does `T` come from?" TypeScript wonders. It's 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... | ||
So, as long as the generic to be inferred is invariant, TypeScript will be unable to infer it. Another simple example would be this: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explaining invariant too like I explained covariant and contravariant above.
So, as long as the generic to be inferred is invariant, TypeScript will be unable to infer it. Another simple example would be this: | |
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: |
|
||
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? | ||
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(...)`. However, Zustand exists. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I mean it's impossible to write Zustand's create
I mean it's impossible to write a value of the type of typeof create
. So it's not create(...)
but create
. (I'm not talking about create(...)
vs create<T>()(...)
but create
itself).
The original copy may seem have a bit non-formal tone but it's important to capture the absurdity of the statement "impossible to write Zustand's create
".
If I want to explain what I'm trying to explain I'll have to write a lot more words and delve into a lot more type theory. Which would be diverging here, hence to keep it succinct I've written it like so.
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(...)`. However, Zustand exists. | |
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`... Wait but Zustand exists? So what do we mean by that? |
@@ -67,9 +67,9 @@ const useBoundStore = create<{ foo: number }>()((_, get) => ({ | |||
})) | |||
``` | |||
|
|||
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`. | |||
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 should not 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. That value is eventually `get`, but first it is `() => undefined`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making the ending of my original copy a bit more clear.
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 should not 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. That value is eventually `get`, but first it is `() => undefined`. | |
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 should not 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 is eventually that, but first it is `() => 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. | ||
Long story short, Zustand has a strange runtime behavior that can not be typed in a sound and inferrable way. We could make it inferrable with the right TypeScript features. However, those do not exist yet. And the strange behaviour is not a problem. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not "strange" it's "crazy". It's crazy in context of type-theory, ie you won't be able to write such code in a more strict statically typed language, maybe "wild" is a more decent way to put it.
Also made the original statement simpler.
Long story short, Zustand has a strange runtime behavior that can not be typed in a sound and inferrable way. We could make it inferrable with the right TypeScript features. However, those do not exist yet. And the strange behaviour is not a problem. | |
Long story short, Zustand has a bit type-theoretically wild runtime behavior, which can not 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 is not a problem. |
|
||
<br/> | ||
|
||
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 `useBearStore` still has the correct type, ie for example `useBearStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. | ||
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 are typed as if the state is the first parameter. In fact, the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. For example, `get` from the second parameter has type `() => { bears: number }` and that is a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useBearStore` still has the correct type; for example, `useBearStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"when in fact" is a single phrase, and it's good to keep the lie and the truth in one sentence.
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 are typed as if the state is the first parameter. In fact, the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. For example, `get` from the second parameter has type `() => { bears: number }` and that is a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useBearStore` still has the correct type; for example, `useBearStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. | |
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 are typed as if the state is the first parameter, when in fact the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. For example, `get` from the second parameter has type `() => { bears: number }` and that is a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useBearStore` still has the correct type; for example, `useBearStore.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 `useBearStore.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). | ||
It is technically not lie because `{ bears: number }` is still a subtype of `{ bears: number, increase: (by: number) => void }`. Therefore, there will be no problem in most cases. You should just be careful while using replace. For example, `set({ bears: 0 }, true)` would compile but will be incorrect as it will delete the `increase` function. If you set from "outside" i.e. `useBearStore.setState({ bears: 0 }, true)` then it will not compile because the "outside" store knows that `increase` is missing. Another instance where you should be careful is if you use `Object.keys`. `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]` (the return type of `get` can make you fall for this). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you set from "outside" i.e.
useBearStore.setState({ bears: 0 }, true)
then it will not compile because the "outside" store knows thatincrease
is missing
It'd still compile, I wrote this when we had strict replace but then we removed it. (Reference for background: #944)
And it is "unsound" ie it can produce a runtime exception.
It is technically not lie because `{ bears: number }` is still a subtype of `{ bears: number, increase: (by: number) => void }`. Therefore, there will be no problem in most cases. You should just be careful while using replace. For example, `set({ bears: 0 }, true)` would compile but will be incorrect as it will delete the `increase` function. If you set from "outside" i.e. `useBearStore.setState({ bears: 0 }, true)` then it will not compile because the "outside" store knows that `increase` is missing. Another instance where you should be careful is if you use `Object.keys`. `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]` (the return type of `get` can make you fall for this). | |
It is not really lie because `{ bears: number }` is still a subtype of `{ bears: number, increase: (by: number) => void }`. Therefore, there will be no problem in most cases. You should just be careful while using replace. For example, `set({ bears: 0 }, true)` would compile but will be unsound as it will delete the `increase` function. Another instance where you should be careful is if you use `Object.keys`. `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]`. The return type of `get` can make you fall for these mistakes. |
@@ -190,11 +190,11 @@ const useBearStore = create<BearState>()( | |||
) | |||
``` | |||
|
|||
Also it's recommended to use `devtools` middleware as last as possible, in particular after `immer` middleware, ie it should be `immer(devtools(...))` and not `devtools(immer(...))`. The reason being that `devtools` mutates the `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) mutate `setState` before `devtools`. | |||
Also, we recommend using `devtools` middleware as an argument, rather than passing arguments to it. For example, when you use it with `immer` as a middleware, it should be `immer(devtools(...))` and not `devtools(immer(...))`. This is because`devtools` mutates the `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) mutates `setState` before `devtools`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we recommend using
devtools
middleware as an argument, rather than passing arguments to it
This sounds a bit wrong, even if you use devtools
at the end you're still passing an argument to it namely your state creator.
I think the original wording is better, I'm fine if someone can come up with a better one but the one is PR isn't really making the cut imo.
Also "mutate" was correct because it's "middlewares".
Also, we recommend using `devtools` middleware as an argument, rather than passing arguments to it. For example, when you use it with `immer` as a middleware, it should be `immer(devtools(...))` and not `devtools(immer(...))`. This is because`devtools` mutates the `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) mutates `setState` before `devtools`. | |
Also, we recommend using `devtools` middleware as last as possible. For example, when you use it with `immer`, it should be `immer(devtools(...))` and not `devtools(immer(...))`. This is because `devtools` mutates `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) also mutate `setState` before `devtools`. |
|
||
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). | ||
For a usual statically typed language, this is impossible. However, with TypeScript, Zustand has something called a "higher kind mutator" that makes this possible. If you are dealing with complex type problems, like typing a middleware or using the `StateCreator` type, you will have to understand this implementation detail. For this, you can [check out #710](https://github.com/pmndrs/zustand/issues/710). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original phrasing "thanks to" is better as it implies it's a TypeScript feature more significantly than a mere "with".
And it's called "higher kinded" and not "higher kind". Putting a hyphen to make it more correct.
For a usual statically typed language, this is impossible. However, with TypeScript, Zustand has something called a "higher kind mutator" that makes this possible. If you are dealing with complex type problems, like typing a middleware or using the `StateCreator` type, you will have to understand this implementation detail. For this, you can [check out #710](https://github.com/pmndrs/zustand/issues/710). | |
For a usual statically typed language, this is impossible. But thanks to TypeScript, Zustand has something called a "higher-kinded mutator" that makes this possible. If you are dealing with complex type problems, like typing a middleware or using the `StateCreator` type, you will have to understand this implementation detail. For this, you can [check out #710](https://github.com/pmndrs/zustand/issues/710). |
@Exalted100 Can you please take the above comments into account? |
Related Issues
Fixes #.
Summary
Reviewed the following documentation:
Check List
yarn run prettier
for formatting code and docs