skip to Main Content

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


  1. Chosen as BEST ANSWER

    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:

    setState((prevState): ComponentState => {
        ...prevState,
        bar: true,
        help: 'Typescript correctly performs excess property checking'
    });
    

  2. 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 satisfies ComponentState 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:

    function MyComponent() {
      const [state, setState] = React.useState<ComponentState>({ foo: 'foo', bar: false });
    
      const onAction = () => {
        setState((prevState) => {
          const result: ComponentState = {
            ...prevState,
            bar: true,
            help: 'why is this allowed?'
          };
    
          return result;
        });
      }
    }
    

    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:

    Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search