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 }
});
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.There are a few solutions:
Or
There is an open issue in git, take a look: cannot setState with dynamic key name type-safe