question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Computed property key names should not be widened

See original GitHub issue

TypeScript Version: 2.1.5

Code The latest @types/react (v15.0.6) use Pick<S,K> to correctly type the setState method of React.Components. While this makes it now possible to merge the state of a component instead of replacing it, it also makes it harder to write a dynamic update function that uses computed properties.

import * as React from 'react';

interface Person {
  name: string;
  age: number|undefined;
}

export default class PersonComponent extends React.Component<void, Person> {
  constructor(props:any) {
    super(props);

    this.state = { 
      name: '',
      age: undefined
    };
    this.handleUpdate = this.handleUpdate.bind(this);
  }

  handleUpdate (e:React.SyntheticEvent<HTMLInputElement>) {
    const key = e.currentTarget.name as keyof Person;
    const value = e.currentTarget.value;
    this.setState({ [key]: value }); // <-- Error
  }

  render() {
    return (
      <form>
        <input type="text" name="name" value={this.state.name} onChange={this.handleUpdate} />
        <input type="text" name="age" value={this.state.age} onChange={this.handleUpdate} />
      </form>
    );
  }
}

The above should show an actual use case of the issue, but it can be reduced to:

const key = 'name';
const value = 'Bob';
const o:Pick<Person, 'name'|'age'> = { [key]: value };

which will result in the same error. Link to the TS playground

Expected behavior: No error, because key is a keyof Person, which will result in the literal type "name" | "age". Both values that are valid keys forstate.

Actual behavior: The compiler will throw the following error:

[ts] Argument of type '{ [x: string]: string; }' is not assignable 
     to parameter of type 'Pick<Person, "name" | "age">'.
       Property 'name' is missing in type '{ [x: string]: string; }'.

My uninformed guess is that the constant key is (incorrectly) widened to string.

Issue Analytics

  • State:open
  • Created 7 years ago
  • Reactions:102
  • Comments:34 (13 by maintainers)

github_iconTop GitHub Comments

24reactions
timroescommented, Aug 12, 2018

A small note here, that @lucasleong solution introduces a possible runtime bug (and a very subtle one too) in React, since State updates may be asynchronous.

You should never use this.state to calculate the new state using the this.setState({ ... }) syntax! The more correct solution would be using an updater function for the state instead, so we can make sure we always spread the correct up-to-date state into the new state:

private handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    this.setState((prevState) => ({
        ...prevState,
        [e.target.name]: e.target.value
    }));
}

I think there is actually a better method to type this right now using mapped types, which doesn’t even change any runtime behavior (as the state spread version does):

private handleChange = <T extends keyof IState>(event: React.ChangeEvent<HTMLInputElement>) => {
  const newState = {
    [event.target.name]: event.target.value,
    // keyNotInState: '42', -> would throw a compile time error
    // numericKeyInState: 'assigning wrong type' -> would throw a compile time error
  };
  this.setState(newState as { [P in T]: IState[P]; });
}

That way the compiler is even able to catch if you are accidentally trying to assign wrong keys explicitly in the newState object, which the spread previous state method would also silently allow (since the whole required state is already in the object, via the spread).

19reactions
lucasleongcommented, Aug 15, 2018

@AlbertoAbruzzo I solved this by first expanding the current state, using the spread operator. Then apply computed property name to update the state like normal. This method seems to work without giving an error.

For example, using @oszbart’s example:

private handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    this.setState({
        ...this.state,
        [e.target.name]: e.target.value
    });
}

This solution has a possible runtime bug, see @timroes solution for a workaround!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Creating object literal with generic key fails to compile
Things work fine when your computed key is of a single string literal type: ... the type gets widened to a string index...
Read more >
Computed Property Names in JavaScript - ui.dev
In this post you'll learn how to have an expression be computed as a property name on an object in JavaScript using ES6's...
Read more >
Documentation - TypeScript 2.9
String-like properties of an object type are those declared using an identifier, a string literal, or a computed property name of a string...
Read more >
Properties - Xojo documentation
A computed property can be expanded in the Navigator to display the Get and Set sections below the property name. You add code...
Read more >
Properties — The Swift Programming Language (Swift 5.7)
Property observers can be added to stored properties you define yourself, ... If a computed property's setter doesn't define a name for the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Hashnode Post

No results found