allow typescript compiler to call setState on only one react state property
ReactjsTypescriptReactjs Problem Overview
I'm using Typescript with React for a project. The Main component gets passed state with this interface.
interface MainState {
todos: Todo[];
hungry: Boolean;
editorState: EditorState; //this is from Facebook's draft js
}
However, the code below (only an excerpt) won't compile.
class Main extends React.Component<MainProps, MainState> {
constructor(props) {
super(props);
this.state = { todos: [], hungry: true, editorState: EditorState.createEmpty() };
}
onChange(editorState: EditorState) {
this.setState({
editorState: editorState
});
}
}
The compiler complains that, in the onChange
method where I am only trying to setState for one property, the property todos
and the property hungry
is missing in type { editorState: EditorState;}
. In other words, I need to set the state of all three properties in the onChange
function to make the code compile. For it to compile, I need to do
onChange(editorState: EditorState){
this.setState({
todos: [],
hungry: false,
editorState: editorState
});
}
but there's no reason to set the todos
and the hungry
property at this point in the code. What is the proper way to call setState on only one property in typescript/react?
Reactjs Solutions
Solution 1 - Reactjs
Edit
The definitions for react were updated and the signature for setState
are now:
setState<K extends keyof S>(state: Pick<S, K>, callback?: () => any): void;
Where Pick<S, K>
is a built-in type which was added in Typescript 2.1:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
See Mapped Types for more info.
If you still have this error then you might want to consider updating your react definitions.
Original answer:
I'm facing the same thing.
The two ways I manage to get around this annoying issue are:
(1) casting/assertion:
this.setState({
editorState: editorState
} as MainState);
(2) declaring the interface fields as optional:
interface MainState {
todos?: Todo[];
hungry?: Boolean;
editorState?: EditorState;
}
If anyone has a better solution I'd be happy to know!
Edit
While this is still an issue, there are two discussions on new features that will solve this problem:
Partial Types (Optionalized Properties for Existing Types)
and
More accurate typing of Object.assign and React component setState()
Solution 2 - Reactjs
Update state explicitly, example with the counter
this.setState((current) => ({ ...current, counter: current.counter + 1 }))
Solution 3 - Reactjs
I think that the best way to do it is to use Partial
Declare your component in the following way
class Main extends React.Component<MainProps, Partial<MainState>> {
}
Partial automatically changes all of the keys to optional.
Solution 4 - Reactjs
We can tell setState
which field it should expect, by parametrising it with <'editorState'>
:
this.setState<'editorState'>({
editorState: editorState
});
If you are updating multiple fields, you can specify them like this:
this.setState<'editorState' | 'hungry'>({
editorState: editorState,
hungry: true,
});
although it will actually let you get away with specifying only one of them!
Anyway with the latest TS and @types/react
both of the above are optional, but they lead us to...
If you want to use a dynamic key, then we can tell setState
to expect no fields in particular:
this.setState<never>({
[key]: value,
});
and as mentioned above, it doesn't complain if we pass an extra field.
Solution 5 - Reactjs
Edit: DO NOT USE this solution, prefer https://stackoverflow.com/a/41828633/1420794 See comments for details.
Now that spread operator has shipped in TS, my preferred solution is
this.setState({...this.state, editorState}); // do not use !
Solution 6 - Reactjs
I don't like casting and workarounds that gives you a lot place to make a mistake while you're scaling your project. Here'is my solution.
-
We have to get proper intellisense for key by using generic type >
<K extends keyof MainState>
-
Then, we have to get proper type for value based on entered key >
typeof MainState[K]
-
In order to get code without and an error and without using casting/workarounds etc, pass callback into
this.setState
-
Combining all together
onChange = <K extends keyof MainState>(fieldName: K, value: typeof MainState[K]) => {
this.setState((prevState) => {
return {
...prevState,
[fieldName]: value,
};
});
};