TypeScript: Computed property key names should not be widened
TypeScript Version: 2.1.5
Code
The latest @types/react (v15.0.6) use Pick<S,K> to correctly type the setState method of React.Components. While this makes it now possible to merge the state of a component instead of replacing it, it also makes it harder to write a dynamic update function that uses computed properties.
import * as React from 'react';
interface Person {
name: string;
age: number|undefined;
}
export default class PersonComponent extends React.Component<void, Person> {
constructor(props:any) {
super(props);
this.state = {
name: '',
age: undefined
};
this.handleUpdate = this.handleUpdate.bind(this);
}
handleUpdate (e:React.SyntheticEvent<HTMLInputElement>) {
const key = e.currentTarget.name as keyof Person;
const value = e.currentTarget.value;
this.setState({ [key]: value }); // <-- Error
}
render() {
return (
<form>
<input type="text" name="name" value={this.state.name} onChange={this.handleUpdate} />
<input type="text" name="age" value={this.state.age} onChange={this.handleUpdate} />
</form>
);
}
}
The above should show an actual use case of the issue, but it can be reduced to:
const key = 'name';
const value = 'Bob';
const o:Pick<Person, 'name'|'age'> = { [key]: value };
which will result in the same error. Link to the TS playground
Expected behavior:
No error, because key is a keyof Person, which will result in the literal type "name" | "age". Both values that are valid keys forstate.
Actual behavior: The compiler will throw the following error:
[ts] Argument of type '{ [x: string]: string; }' is not assignable
to parameter of type 'Pick<Person, "name" | "age">'.
Property 'name' is missing in type '{ [x: string]: string; }'.
My uninformed guess is that the constant key is (incorrectly) widened to string.
About this issue
- Original URL
- State: open
- Created 7 years ago
- Reactions: 113
- Comments: 34 (13 by maintainers)
A small note here, that @lucasleong solution introduces a possible runtime bug (and a very subtle one too) in React, since State updates may be asynchronous.
You should never use
this.stateto calculate the new state using thethis.setState({ ... })syntax! The more correct solution would be using an updater function for the state instead, so we can make sure we always spread the correct up-to-date state into the new state:I think there is actually a better method to type this right now using mapped types, which doesn’t even change any runtime behavior (as the state spread version does):
That way the compiler is even able to catch if you are accidentally trying to assign wrong keys explicitly in the
newStateobject, which the spread previous state method would also silently allow (since the whole required state is already in the object, via the spread).@AlbertoAbruzzo I solved this by first expanding the current state, using the spread operator. Then apply computed property name to update the state like normal. This method seems to work without giving an error.
For example, using @oszbart’s example:
This solution has a possible runtime bug, see @timroes solution for a workaround!
Now that TypeScript supports pattern template literal index signatures, this widening to
stringis even more undesirable because there doesn’t seem to be a way to use such template-pattern keys directly in an object literal:Playground link
FWIW this is the helper function I tend to use when I need stronger types for computed keys:
Which produces these:
Playground link to code
@sebald You might mean something like
In any case, I don’t think
keyshould widen, so there looks like a bug here.I’ve encountered similar problem…
I’m not quite sure if this is (not?) part of the bug, but my scenario looks as follows:
Which ends up with something similar to:
Type '{ [x: string]: string; }' is not assignable to type 'Pick<State, EnumDefinition.NAME | "test". Property 'name' is missing in type '{ [x: string]: string; }'.Before 2.9 this hack
this.setState({[event.target.name as any]: event.target.value});helped to get rid of errors, but now it doesn’t help unfortunately.Problem:
Workaround:
```ts interface If { a: Type; b: Type; }
type S = keyof If; ```
See this tweet.
Fix is up at #21070
Does not work anymore with 2.9.1:
I have to ts-ignore it now:
Any better solution with 2.9.1?
How about this? (SO)
That tells setState that it should expect no properties in particular. And then we pass an extra property, which it doesn’t seem to mind.
Edit: Yeah this doesn’t check the types, or fix this issue. It’s just a quick workaround for anyone who is stuck transpiling, and came here for help.
The string literal change was a nice stopgap but definitely falls short. It would be nice to see this work with
unique symboltypes as well, since you actually must use computed keys.This wouldn’t verify that
valueis of the right type, right?I think this may be related. I’m seeing a problem with this in V3.1.1:
Error text:
@tkrotoff This works for me:
The workaround found here worked for me