3
votes

I'm trying to describe the type of the function which handles change event to reflect the value in the state.

The desired implementation would look like:

handleChange: ChangeHandler<State> = field => value =>
    this.setState({ [field]: value });

Given state:

interface State {
  title: string;
  description: string | null;
}

One could pass this function to the component as:

<TextComponent onChange={this.handleChange('title')} />

My best attempt to tackle this problem was to create the following type:

type ChangeHandler<S> = <K extends keyof S, V extends S[K]>(key: K) => (value: V) => void;

However, this seems to work only if all properties of the state are optional. I can't manage to get this to work with state of both optional and non-optional properties.

Compiler error:

[ts] Argument of type '{ [x: string]: V; }' is not assignable to parameter of type 'State | ((prevState: Readonly, props: Readonly) => State | Pick | null) | Pick<...> | null'.
Type '{ [x: string]: V; }' is missing the following properties from type 'Pick': title, description [2345]

2

2 Answers

2
votes

Root Cause

This is caused by a known issue with TypeScript, where computed property names consisting of a union of literals are widened.

In handleChange, the type of field is widened from 'title' | 'description' to string, which causes this.setState to fail due to the way this.setState's typings are defined.

Below is a simplified definition of setState:

function setState<K extends keyof State>(s: Pick<State, K>): void {
    console.log(s);
}

When applying that definition to handleChange, the K above becomes 'title' | 'description', and the s parameter becomes Pick<State, 'title' | 'description'>, which is essentially equivalent to the State interface.

So, given that our field has been widened to string and this.setState in this context accepts State, we'll get the error you're seeing as we are calling this.setState with the following type:

{ [x: string]: V }

Which is not assignable to State as it's missing the required parameters title and description.

Solution

Our best bet here is to use a type assertion to silence the error while maintaining type checking. The updated definition is below:

let handleChange: ChangeHandler<State> = field => value =>
    setState({ [field]: value } as unknown as State);

let x = handleChange('title');

x('test'); // OK

x(50); // Error: should be string

See this playground link containing the code above.


See discussion on this specific issue on DefinitelyTyped for alternate solutions / approaches.

0
votes

You must not define V extends S[K] as you can directly inline this declaration as follows:

type ChangeHandler<S> = <K extends keyof S>(key: K) => (value: S[K]) => void;