Typescript generic state change function type cast

2020-05-03 12:13发布

问题:

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

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


回答2:

There are a few solutions:

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

Or

 this.setState({ [name]: value } as Pick<IInputStates, keyof IInputStates>);

There is an open issue in git, take a look: cannot setState with dynamic key name type-safe