I’ve noticed unexpected behaviour when using useState and Typescript where it allows any property to be assigned even if it does not exist in the interface. Take the following example:
interface ComponentState {
foo: string,
bar: boolean
}
const [state, setState] = useState<ComponentState>({ foo: 'foo', bar: false });
const onAction = () => {
setState((prevState) => {
...prevState,
bar: true,
help: 'why is this allowed?'
});
}
How comes typescript doesn’t complain about the "help" property? If I try to set bar (a boolean) as a string, it will complain.
Maybe it is something to do with the prevState object spread?
I don’t like this because it allows for bugs to slip through. Am I missing something? I looked at the definitions of useState and I don’t see it allowing [key: string]: any
2
Answers
I am going to keep jered's answer as accepted but I wanted to add that we can have a cleaner solution by simply declaring the return type of the function:
This isn’t specific to React but an artifact of TypeScript’s structural typing system.
In a nutshell, in many cases TypeScript allows the extra property because the object you are returning from your
setState()
function still satisfiesComponentState
even if it has extra properties.On one hand, this is usually fine and doesn’t really affect behavior, because logic elsewhere in your code won’t be allowed to access the extra properties, they’re sort of phantom properties that can’t be accessed anyway — TypeScript wouldn’t let you because they’re not part of the
ComponentState
definition. We usually care much more about ensuring we have the right properties that we expect and need to be there on the object, than having extra properties that were not expected.On the other hand, this isn’t necessarily desirable because you might accidentally add extra properties somewhere thinking they will be visible/usable somewhere else even though they aren’t, or trip up other logic that reads properties and doesn’t expect extra ones. For example, what if you used
Object.keys()
somewhere assuming that the exact properties are known but it turns out there are other ones?A way to get around this could be to use more explicit type definitions, which are usually not needed but can be more useful here. For example:
This will properly give you an error when you try to define
result
.The reason you get an error this way but not when you simply return the type directly is because of excess property checking. TypeScript only does this in certain cases: