0
votes

I'm coding Simple LoginForm with react and typescript.

interface State {
    username: string;
    password: string;
}

class LoginForm extends React.Component<Props, State> {
    readonly state: Readonly<State> = {
        username: '',
        password: ''
    }

    onChange = (key: keyof State) => (e: React.FormEvent<HTMLInputElement>) => {
        const value = e.currentTarget.value;
        this.setState({ [key]: value });  // error
    }

    render() {
        return (
            ...
            <TextField
                ...
                onChange={this.onChange('username')}
            />
            <TextField
                ...
                onChange={this.onChange('password')}
            />
            ...
        )
    }
}

Typescript failed to compile in setState line with is error.

Argument of type '{ [x: string]: string; }' is not assignable to parameter of type 'State | ((prevState: Readonly, props: Props) => State | Pick'. Property 'username' is missing in type '{ [x: string]: string; }'.

The 'key' parameter in this.onChange function only allows either 'username' or 'password' as I expected. This means that typescript knows typeof 'key' parameter must be either 'username' or 'password'. This is obvious because I defined it as (key: key of State).

But why in setState function, typeof 'key' parameter changes to just normal string?

Thanks.

3

3 Answers

1
votes

You have run into a known TypeScript bug where a computed property of a union type is widened to an index signature. It looks like this will be fixed for TypeScript 3.0, but that's not certain.

The workaround you should use for now is just to assert that the object is of the expected type:

this.setState({ [key]: value } as Record<typeof key, typeof value>);

I'm not familiar enough with React to know if setState() is expecting a full State, but be warned that { [key]: value } is a Partial<State> since it won't contain both keys... and TypeScript won't catch this because key is a union type. So be careful at runtime. You might want to do the assertion as

this.setState({ [key]: value } as Partial<State>);

and if that causes an error, it is a real error you should fix.

Hope that helps. Good luck!

1
votes

Try this code:

onChange = (key: keyof IState) => (event: React.FormEvent<HTMLInputElement>) => {
    this.setState({
      [key]: event.currentTarget.value
    } as Pick<IState, keyof IState>);
}
0
votes

Adding this as an alternative as this solved my issue.

It seems that when setting the state this.setState({prop: value}) React TS can sometimes expect the full stateas speculated by @jcalz on the selected answer.

this.setState({
      ...this.state,
      [key]: value
});

More information about best practices regarding this issue here: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/26635