Skip to content

On the use of Pick for @types/react's setState #18365

@Jessidhia

Description

@Jessidhia

I understand that Pick was used for the type of setState because returning undefined for a key that should not be undefined would result in the key being set to undefined by React.

However, using Pick causes other problems. For one, the compiler service's autocomplete becomes useless, as it uses the result of the Pick for autocompletion, and when you request completions the Pick's result doesn't yet contain the key you may want to autocomplete. But the problems are particularly bad when writing setState with a callback argument:

  1. The list of keys is derived from your first return statement; if you don't return a particular key in your return statement you also can't read it in the argument without forcing the list of keys to reset to never. Multiple return statements can be hard to write if they return different keys, especially if you have an undefined return somewhere (e.g. if (state.busy) { return }).
    • This can be worked around by always using the input in a spread (e.g. this.setState(input => ({ ...input, count: +input.count + 1 }))) but this is redundant and a deoptimization, particularly for larger states, as setState will pass the return value of the callback to Object.assign.
  2. If, for some reason, the type you are returning is not compatible with the input type, the Pick will choose never for its keys, and the function will be allowed to return anything. Even keys that coincide with an existing key effectively allow any as a value -- if it doesn't fit, it's just not Picked, and is treated as an excess property for {}, which is not checked.
  3. If never is picked as the generic argument, for any of the reasons listed above, a callback argument may actually be treated as an argument to the object form of setState; this causes the callback's arguments to be typed any instead of {}. I am not sure why this is not an implicit any error.
interface State {
  count: string // (for demonstration purposes)
}

class Counter extends React.Component<{}, State> {
  readonly state: Readonly<State> = {
    count: '0'
  }

  render () {
    return React.createElement('span', { onClick: this.clicked }, this.state.count)
  }

  private readonly clicked = () => {
    this.setState(input => ({
      count: +input.count + 1 // not a type error
      // the setState<never>(input: Pick<State, never>) overload is being used
    }))
  }
}

To sum it up, while the use of Pick, despite some inconvenience, help catch type errors in the non-callback form of setState, it is completely counter-productive in the callback form; where it not only does not do the intended task of forbidding undefined but also disables any type checking at all on the callback's inputs or outputs.

Perhaps it should be changed, at least for the callback form, to Partial and hope users know to not return undefined values, as was done in the older definitions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions