SolvedDefinitelyTyped [@types/react] cannot setState with dynamic key name type-safe

If you know how to fix the issue, make a pull request instead.

If you do not mention the authors the issue will be ignored.

I cannot call setState with an object being created from computed property name with type-safety:

type State = {
  username: string,
  password: string
};

type StateKeys = keyof State;

class A extends React.Component<{}, State> {
  dynSetState(key: StateKeys, value: string) {
    this.setState({
      [key]: value // Error here. Pretty sure key is in StateKeys
    });
  }
}

I do aware of #18365, and the workaround in #18365 (comment) . However, when using the workaround, Typescript doesn't error out when it should:

  dynLooselySetState(key: string, value: string) {
    this.setState(prevState => ({
      ...prevState,
      [key]: value // No error here, but can't ensure that key is in StateKeys
    }));
  }
14 Answers

✔️Accepted Answer

This is a limitation of the compiler itself, inferred types via computed property keys do not support union types, it only supports string, number, symbol or a single literal of those 3, if it detects that it's a union of any of those it will just coerce it into the general string type. The workaround here would be to coerce the object:

dynSetState(key: StateKeys, value: string) {
  this.setState({
    [key]: value
  } as Pick<State, keyof State>)
}

This will still give an error appropriately if the value is not in the set of possible property value types, but will not give you an error if the keys are not within the set of possible property keys.

Other Answers:

exactly what @ferdaber although IMHO casting stuff like this is not very "good pattern", overall you should prefer updating state via callback pattern which again adheres to best practices, like extracting that callback outside the class to pure easy to test function :)

Good:

class C extends Component<{}, State> {
  updateState(key: StateKeys, value: string) {
    this.setState((prevState) => ({
      ...prevState,
      [key]: value,
    }));
  }
}

Better:

const updateState = <T extends string>(key: keyof State, value: T) => (
  prevState: State
): State => ({
  ...prevState,
  [key]: value
})

class C extends Component<{}, State> {
  doSomething(){
    this.setState(updateState('password','123124'))
  }
}

I had to add as unknown to @ferdaber's solution:

  this.setState({
    [key]: value
  } as unknown as Pick<State, keyof State>)

That warned if key was a boolean, but not if it was a number!

So I went for this shorter solution:

  this.setState<never>({
    [key]: value
  })

This warns if key is a boolean or a number.

Indeed, what I'm trying to accomplish is to do something like this: https://reactjs.org/docs/forms.html#handling-multiple-inputs
Which will make it easier to deal with a form with multiple inputs. I may still have to make sure that the "name" attribute is what we expect, but after that type-safety should works.

Here is the discussion, the design notes from the TS team, and an attempt at fixing it (which I believe was retracted for a future version):
microsoft/TypeScript#13948
microsoft/TypeScript#18155
microsoft/TypeScript#21070

More Issues: