Computed property key names should not be widened
See original GitHub issueTypeScript Version: 2.1.5
Code
The latest @types/react
(v15.0.6
) use Pick<S,K>
to correctly type the setState
method of React.Component
s. 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
.
Issue Analytics
- State:
- Created 7 years ago
- Reactions:102
- Comments:34 (13 by maintainers)
Top GitHub Comments
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.state
to 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
newState
object, 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!