Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pick<S, K> type with dynamic/computed keys

The latest @types/react (v15.0.6) make use of features added in TypeScript 2.1 for setState, namely Pick<S, K>. Which is a good thing, because now the typings are correct, because before the update typings "didn't know" that setState is merging this.state, rather than replacing it.

Also, using Pick makes the setState function very strict in terms of allowed input. It is no longer possible to add properties to the state that aren't defined in the component definition (second generic of React.Component.

But it is also harder to define a dynamic update handler. For example:

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 });
  }

  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 setState function 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; }'.

even though the type of key is "name" | "age".

I can not find a solution for this, other than having a separate updateName and updateAge function. Does anyone know how to use Pick with dynamic key values?

like image 403
Sebastian Sebald Avatar asked Feb 07 '17 12:02

Sebastian Sebald


2 Answers

The answer from Sebastian no longer works for me (though at one stage it did). I now do the following until it is resolved in Typescript core (as per the issue listed in Sebastians answer):

  handleUpdate (e:React.SyntheticEvent<HTMLInputElement>) {
    const newState = {};
    newState[e.currentTarget.name] = e.currentTarget.value;
    this.setState(newState);
  }

It's lame, but it works

like image 87
Chris Avatar answered Oct 20 '22 10:10

Chris


So after doing more research I can provide a little more context on what is happening in the above code.

When you do something like const name = 'Bob' the type of the variable name is 'Bob' not string. However, if you replace the const with a let (let name = 'Bob') the variable name will be of type string.

This concept is called "widening". Basically, it means that the type system tries to be as explicit as possible. Because const can not be reassigned TypeScript can infer the exact type. let statements can be reassigned. Thus, TypeScript will infer string (in the above example) as the type of name.

The same is happening with const key = e.currentTarget.name as keyof Person. key will be of (union) type "name"|"age", which is exactly what we want it to be. But in the expression this.setState({ [key]: value }); variable key is (incorrectly) widened to a string.


tl;dr; It seems like there is a bug in TypeScript. I posted the issue to the Github repo and the TypeScript team is investigating the problem. :)

As a temporary workaround you can do:

this.setState({ [key as any]: value });
like image 23
Sebastian Sebald Avatar answered Oct 20 '22 10:10

Sebastian Sebald