Search code examples
reactjsformstypescriptdry

Form in React and TypeScript with DRY handle of change


I looking for best way manage state of form in React with TypeScript. My simple form have two values: login and password fields. I implement IState interface for form state and DRY handleChange method for store new value in state without recreating function in each render() execution.

interface IState {
  login: string;
  password: string;
}

class LoginForm extends React.Component<{}, IState> {
  public state = {
    login: "",
    password: "",
  };

  public render() {
    const { login, password } = this.state;
    return (
      <form>
        <input name="login" value={login} onChange={this.handleChange}/>
        <input name="password" value={password} onChange={this.handleChange} type="password"/>
      </form>
    );
  }

  private handleChange = (e: React.FormEvent<HTMLInputElement>) => {
    const { name, value } = e.currentTarget;
    const n = name as keyof IState;
    // @ts-ignore
    this.setState({
      [n]: value,
    });
  }
}

I use native HTML's name attribute for store field name. n variable will have only login or password value. Any other value is impossible. Can I tell TypeScript the n variable is "login" | "password" literals type? TypeScript regard n as string type variable even I use:

const n = name as keyof IState;

or

const n = name as "login" | "password";

Then, when I remove // @ts-ignore I get error:

Type error: Argument of type '{ [x: string]: string; }' is not assignable to parameter of type 'IState | Pick<IState, "login" | "password"> | ((prevState: Readonly<IState>, props: Readonly<IProps>) => IState | Pick<IState, "login" | "password"> | null) | null'.
Type '{ [x: string]: string; }' is missing the following properties from type 'Pick<IState, "login" | "password">': login, password  TS2345

but no error when I hardcode:

const n = "login";

Any way to force "login" | "password" type to n variable? Or any other solution for DRY handleChange without optimization issues and pure TypeScript?


Solution

  • We can use Pick to ensure that you're setting a key that has been defined in your IState interface.

    Pick - set state

    private handleChange = (e: React.FormEvent<HTMLInputElement>) => {
        const { name, value } = e.currentTarget;
        this.setState({
            [name]: value
        } as Pick<IState, keyof IState>);
    };
    

    Or alternative you can use Partial which will make all your state keys optional.

    class App extends React.Component<{}, Partial<IState>> {
        // ... rest of component
    }