TypeScript: Partial does not solve React.setState
Traditionally in TS, the absence of value or an explicit undefined value was exactly the same thing. Perhaps it still even makes sense for Partial, but I thought Partial was meant to solve the React setState problem; and I believe it’s still not solved today.
TypeScript Version: 2.1.4
Code
type State = { veryImportant: number }
// This compiles fine with all the mandatory flags (strictNullChecks, etc)
// if setState uses Partial<State>
// This is a huge invariant violation and can happen very easily for instance
// by setting `veryImportant` to a nullable variable.
this.setState({ veryImportant: undefined })
Expected behavior: A non nullable property should not be updatable with null/undefined with strictNullChecks.
We need to be able to say "give me an object that
- Either do not declare a property I have
- Or if it does, must have the same exact type (not nullable)
By the way, I don’t even use React, but this is a fairly common and useful behavior to have.
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 3
- Comments: 42 (25 by maintainers)
Commits related to this issue
- add Partial typing to setState this is not strict enough for strictNullChecks see: https://github.com/Microsoft/TypeScript/issues/12793 — committed to yihongang/inferno by yihongang 7 years ago
You guys are the best. Thank you. (By the way, mapped types are an amazing feature; I love them. In fact, 2.1 is an amazing release all around. Ya’ll are killing it over there).
I believe this is what you’re looking for:
sure thing mapped types got some love, let’s not skip the legs day tho
I just want to remark that the rapidity of this feedback loop is awesome! An issue comes up, is discussed and gets resolved in less than 2 days, and this is a programming language we are talking about. Great work!
I agree with @lukecwilliams’s not getting how this is solved, especially given the title of this issue.
Partialstill won’t work withsetState, sincesetStateusesPick. Please reopen @ahejlsberg or describe howPartial< T >is compatible withPick< T, ... >, or conclude thatsetStateshould have another signature. Anything but leaving it closed when it’s still an issue.The following does not work, and is a not-so-uncommon way to partially build up a state somewhere, to then apply it with
setState. Please considerfooto be a lot more complex and not as trivial as this example, where alternative logic would obviously suffice.It fails with:
Yes, one can Hawaii-cast things to make
tschappy, à la:or, pick your poison:
but this is not a solution. Or is it? I’m personally resentful to blatantly lie about types this way. It’s a bad habit.
We’ve been discussing this and have come to the consensus that
Pick<T, K>should copy the modifiers fromTwhich it currently doesn’t do. More specifically, for a mapped type{ [P in keyof T]: X }we currently copy the modifiers fromT, and we want to do the same for a mapped type{ [P in K]: X }whereKis a type parameter with the constraintK extends keyof T.Ok, those examples from @ericanderson and @RyanCavanaugh are helpful. I now see that there are clear differences between “no key” and “key: undefined”.
Damnit, you know what this means?
It means almost all of the interfaces in all of the code I’ve ever written are maltyped.
Pretty much wherever I have said,
{ foo?: T }, I meant to say{ foo?: T | undefined }. Because {} is assignable to{ foo?: T }, I have to treatfooasT | undefinedat runtime. With strictNullChecks I’m actually required to. Since I’m already doing that, I usually - read almost always - want to allow myself to assignundefinedto foo.I can’t think of a single place in my code where I’ve made an optional property but wish for my compiler to insist that if the property exists then it must not be
undefined. But that’s my problem, not TS’s problem. TS is just following JS here, and JS clearly makes a distinction.With that in mind, I now am leaning towards Pick as the correct expression of setState, and I just need to update all of my interfaces.
I think the problem is that the symmetry between
{ foo: string | undefined }and{ foo?: string }is a lie – spreading in{}to an object has different effects than spreading in{ foo: undefined }.In an ideal world there would be a difference
We’ve sort of swept this under the rug, but with Object spread and
Object.keys, there really is a difference between “missing” and “present with valueundefined”. It ought to be possible to express this difference to indicate your intent.@RyanCavanaugh If there is a bug here, I would suggest that any parameter who is an optional is also an implicit “T | undefined” so that when you convert it in something like
Pick, the ability to assignundefineddoes not get lost.