-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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(types)!: require complete state if setState
's replace
flag is set
#2580
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
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. |
I believe this is expected behavior. See "overwriting state" on the Readme. Thank you |
However, I do advocate for this change. |
Oh, that sounds pretty bad. |
I disagree with defaulting to |
It's not about changing the logic. It's just about typing. Yet... |
Hmm, maybe it's fine. |
Three suggestions to fix the order of overloads:
- replace: true,
+ replace: true | undefined, Downside: true | undefined might confuse users.
- replace?: false | undefined,
+ replace?: false,
- replace?: false | undefined,
+ replace?: false,
...
+ _(
+ partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
+ replace?: undefined,
+ ): void Downside: Somewhat unnecessary third overload. |
My preference: Original or Variant 3. |
Actually: this does not seem to affect function arguments! Variant 2 + exactOptionalPropertyTypes enabled As far as I can tell from my testing, removing the explicit |
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.
LGTM
replace?: boolean | undefined, | ||
replace?: false, |
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.
I see. I too thought | undefined
is required somehow, but this is functions argument, not object property.
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.
See CI errors 😄
This fix breaks the following code: const wrapper: StoreApi<T>["setState"] = (partial, replace) => {
return store.setState(partial, replace);
} I can imagine this kind of code existing in other libraries or userland... Any ideas? |
Yeah, function overloading is always tricky. No, I don't have any ideas. |
@Yonom I think we can merge this for v5, and based on feedback, we might relax the type in the future. |
The old typescript tests were failing because old typescript versions <=5.2 resolve the following to {
(...a: infer A1): infer Sr1;
(...a: infer A2): infer Sr2;
} I updated the types of devtools and immer middleware to also specify two overloads for their wrapped setState function, solving this issue (and improving the types for these middleware at the same time) There is another solution to this problem, which is to first infer the parameters and return types as a tuple array, then filter the unknown types and finally turn that into a union. microsoft/TypeScript#32164 (comment) |
After the last commit (update setState types for devtools and immer), the following test is failing: it('StateCreator<T, [StoreMutatorIdentfier, unknown][]> is StateCreator<T, []>', () => {
interface State {
count: number
increment: () => void
}
const foo: <M extends [StoreMutatorIdentifier, unknown][]>() => StateCreator<
State,
M
> = () => (set, get) => ({
count: 0,
increment: () => {
set({ count: get().count + 1 }) // <-- type error here
},
})
create<State>()(persist(foo(), { name: 'prefix' }))
})
It seems like typescript is having issues consolidating multiple generic functions type Action =
| string
| {
type: string
[x: string | number | symbol]: unknown // <-- allows for arbitrary values
} This is another minor breaking change, I would love to avoid this incase you have a better idea for a workaround |
All tests are passing again and I tested also with old typescript 4.5.5. Ready for review @dai-shi |
FWIW there's already a PR with the same goal by me here. This PR's approach would break things like this... let foo = Math.random() > 0.5 ? true : false
store.setState({ bears: 5 }, foo) // error Here's a minimal reproduction... declare const f: {
(a: true): void
(a: false): void
}
let foo = Math.random() > 0.5 ? true : false
f(foo) // error |
@devanshj Thanks for pointing this out. The following cases exist:
This PR breaks type support for cases 3 and 4. I assume the distribution of real world uses of each of these cases to be:
Case 3 is unfortunate. In my opinion, breaking case 3 to catch errors in case 2 is well worth it. (Case 3 is easily worked around) Workaround for case 3: - store.setState(partialOrFull, replace);
+ store.setState(partialOrFull, replace as any); We could add support for case 4: type SetStateInternal<T> = {
_(
partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
replace?: false,
): void
_(
state: T | { _(state: T): T }['_'],
- replace: true,
+ replace: boolean,
): void
}['_'] Pro: fixes case 4 I don't see the point in case 4 and therefore suggest keeping |
Oh, yeah... Nothing is ideal.
How is it? I think it should be documented somewhere in |
@Yonom ☝️ Sorry, if I wasn't clear. Can you add a note in docs and v5 migration guide please? |
@Yonom A friendly reminder. |
Sorry about the delay. Done @dai-shi |
Thanks. It's my bad #2138 (comment), but the migration doc is under Can you move your notes into it? |
I also missed that! updated now |
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.
Looks great to me! Thanks for your contribution!
Whew! Thanks a lot for all the guidance along the way, this was way more complicated than originally expected 😀 |
Related Issues or Discussions
Fixes #2578
Summary
Update the
setState
type to conditionally require a non-partialT
if thereplace
argument of the function is set to true. This change also updates the types forset
insidecreate((set) => ({ ... }))
callbacks.Previously,
store.setState({}, true)
was considered valid, even though it would result in an invalid state. This fixes that issue.My suggestion is to consider this a breaking change to be on the safe side.
Check List
pnpm run prettier
for formatting code and docs