Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React w/ TypeScript: why is the type for the state lost when useState with a callback as the updater

First of all the code snippet you are going to see is a contrived example. I am just curious as to why the typing doesn't work.

interface MyState {
  a: boolean;
  b: number;
}

const defaultState: MyState = {
  a: false,
  b: 1
};

const keys = Object.keys(defaultState) as Array<keyof MyState>;

export default function App() {
  const [myState, setMyState] = React.useState<MyState>(defaultState);

  React.useEffect(() => {
    keys.forEach((key) => {
      setMyState((prev) => ({
        ...prev,
        [key]: "lol". // should've given an error but it didn't
      }));
    });
  }, []);

so I have a defaultState and a list of its keys defined outside of the component. In the useEffect I am using the key to update the state(right now it is just changing every value to 'lol'). I passed a callback as the updater in the setMyState and what it returns should be the new state. But here I make the value to be string, which violates the interface I had MyState.

I wonder why it is not triggering the error

This is a live demo: https://codesandbox.io/s/sweet-dewdney-u83id?file=/src/App.tsx

Minimal example without react: Playground

like image 611
Joji Avatar asked Sep 04 '20 00:09

Joji


1 Answers

The issue is not in using a callback to update the state - that's fine. The issue is that when you set a property of an object using a dynamic key, typescript doesn't know what the value of the key is for any given iteration of the loop, let alone what type its value should be. So you need to tell it. You need to dynamically type your keys using generics. There's a great article about it right here.

  function updateValue<T extends keyof MyState, K extends MyState[T]>(
    name: T,
    value: K
  ) {
    setMyState(prev => ({ ...prev, [name]: value }));
  }

  React.useEffect(() => {
    keys.forEach((key) => {
      updateValue(key, "lol");
    });
  }, []);

Here is a working codesandbox

Working Typescript playground

Gives the errors exactly as it should.

Further musings:

There are some interesting quirks about this. What if you set the value to one of the possible values on your interface?

keys.forEach((key) => {
 updateValue(key, 4);  // <---- no error, but there should be
});

In theory, this should also throw an error, because key could be a, in which case the types dont match. TypeScript doesn't throw an error. However, if you specify to run this only when key === 'a', the error does occur:

keys.forEach((key) => {
  if (key === 'a'){
    updateValue(key, 4); // <---- typechecking error occurs
  }
});

In the latter case, typescript is smart enough to know that if the key is a, the type has to be a number. But in the former case, where you are trying to assign a value that matches one of the types on the interface, but not all of them, we may be seeing a bug in, or reaching the limits of typescript.

like image 91
Seth Lutske Avatar answered Nov 12 '22 00:11

Seth Lutske