Typescript generic state change function type casting error

by Remi   Last Updated July 12, 2019 14:26 PM - source

I'm creating a generic function to change the state.

The error message:

TS2345: Argument of type '() => { [x:string]: string }' is not assignable to parameter of type 'IUserSignUpState | Pick<IUserSignUpState, "url" | "errors"> | ((prevState: Readonly<IUserSignUpState>, props: Readonly<IUserSignUpState>) => IUserSignUpState | Pick<...> | null) null'.

Type '() => { [x: string] string; }' is not assignable to type '(prevState: Readonly<IUserSignUpState>, props: Readonly<IUserSignUpState>) => IUserSignupState | Pick<IUserSignUpState>, "url" | "errors" | null'. 

Type '{ [x: string]: string; }' is not assignable to type 'IUserSignUpState | Pick<IUserSignUpState, "url" | "errors"> | null'.

Type '{ [x: string]: string; }' is missing the following properties from type 'Pick<IUserSignUpState, "url" | "errors">': url, errors.

This is the example of the generic change function:

./UserSignUp.tsx


interface IUserSignUpState {
  url: string;
  errors: string[];
  // following can be used, but will lose type checking (*)
  // [key: string]: string;
}

class UserSignUp extends React.Component<{}, IUserSignUpState>
  state = {
    url: '',
    name: '',
    errors: [],
  };

  render() {
    const { url, errors } = this.state;
    return (
      <form>
        <input type="text" name="url" value={url} onChange={this.change} />
        <input type="text" name="name" value={name} onChange={this.change} />
      <form>
    )
  }

  change = (event:React.ChangeEvent<HTMLInputElement>) => {
    const name = event.target.name;
    const value = event.target.value;

    // gives error here:
    this.setState(() => {
      return {
        [name]: value
      }
    })
  }
};

In this example the change event should only be allowed to update url: string; and name: string;, and not errors: [].

Should I be defining the 'url' and 'name' types and reuse that in the change function somehow?


*In the Typescript documentation it states that Indexable types can be used. However, by doing so I will lose type checking. And a side-effect is that I could potentially also set the 'error' state, which typically shouldn't be possible from the change function.


update: based on the answer in this Stackoverflow Question, the following solution is possible:

split up the interface:

interface IInputState {
  url: string;
}

interface IUserSignUpState extends IInputState {
  errors: string[];
}

And re-use that interface, by either:

this.setState({ [name]: value } as Partial<IInputStates>)

or:

const name = event.target.name as keyof IInputStates;
const value = event.target.value;

this.setState(():IInputStates => {
  return { [name]: value }
});


Answers 1


Using types won't prevent the errors property from being changed as the type checking applies at compile time only. This will still run as plain old dynamic JavaScript when it gets to the browser where anything goes.

If you want to restrict the properties the change function acts on you need to check the property name e.g. against an array of allowed values.

As a side note your inputs are missing the name property.

 render() {
    const { url, errors } = this.state;
    return (
      <form>
        <input type="text" name="url" value={url} onChange={this.change} />
        <input type="text" name="name" value={name} onChange={this.change} />
      <form>
    )
  }

  change = (event:React.ChangeEvent<HTMLInputElement>) => {
    const name = event.target.name;
    const value = event.target.value;

    if (!isValidProperty(name)) return;

    this.setState({ [name]: value } as Partial<IInputStates>)
  }

  // Dummy instance to force a compile warning and ensure we capture all the property names
  const inputExample: IInputStates = { url: '' };
  const inputKeys = Object.keys(inputExample);

  function isValidProperty(name: string): name is keyof IInputStates {
    return inputKeys.includes(name);
  }
dezfowler
dezfowler
July 12, 2019 10:19 AM

Related Questions


Component renders twice on async React context update

Updated October 04, 2019 00:26 AM



React Hooks & useContext: Is This a Good Pattern?

Updated February 24, 2019 03:26 AM

State Management in React

Updated July 19, 2019 12:26 PM