14

Say I have a form with multiple input fields. In normal ES6/React I would create a single method that all input fields would point their onChange handlers to. Something like this:

handleChange(e) {
    e.preventDefault();
    this.setState({[e.target.name]: e.target.value});
}

This helps in the case where you have a lot of form elements and don't have to create a specific method to handle each of them.

Is this possible in TypeScript? Even if it is not type-safe?

3
  • Why wouldn't it be possible? Are you getting any errors? what exactly is the problem? Commented Jan 25, 2017 at 23:14
  • Anything you can do in JavaScript, you can do in TypeScript. Commented Jan 26, 2017 at 0:42
  • @NitzanTomer @vutran: It would result in error TS2345 Commented Jan 3, 2021 at 18:54

5 Answers 5

12

As answered in the comments, everything you can do in JavaScript is also valid TypeScript.

However, I guess you're getting errors from the TypeScript compiler (depending on the compiler options you've set).

Assuming your component looks something like this:

interface ComponentProps { }
interface ComponentState {
  name: string
  address: string
}

class MyComponent extends React.Component<ComponentProps, ComponentState> {
  handleChange(e) {
    e.preventDefault()
    this.setState({ [e.target.name]: e.target.value })
  }
}

I'm getting this error:

== External: (30,19): error TS2345: Argument of type '{ [x: number]: any; }' is not assignable to parameter of type 'ComponentState'.

== External: Property 'name' is missing in type '{ [x: number]: any; }'.

And when using the noImplicitAny compiler option (which I like to use), this additional error:

== External: (28,16): error TS7006: Parameter 'e' implicitly has an 'any' type.

If you're sure that your code is correct, you can silence these errors by explicitly casting the parameter for handleChange and the argument for setState

  handleChange(e: any) {
    e.preventDefault()
    this.setState({ [e.target.name]: e.target.value } as ComponentState)
  }
Sign up to request clarification or add additional context in comments.

Comments

10

I solved this problem like this:

handleChange = (field: string) => (event) => {
    this.setState({ [field]: event.target.value } as Pick<State, any>);
};

...

<Input onChange={this.handleChange('myField')} />

The Accepted answer didn't work for me.

Comments

5
import React, { useState } from 'react';

const Form = () => {
  const [inputValues, setInputValues] = useState<{ [x: string]: string }>()

  const handleFormSubmit = async (e: React.MouseEvent<HTMLElement>) => {
    e.preventDefault()
    const data = {
      name: inputValues?.name,
      email: inputValues?.email,
      phone: inputValues?.phone,
      income: inputValues?.name
    }

    const requestOptions = {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    };

    try {
      const response = await fetch('https://xyz/form-submit', requestOptions)
      const res = await response.json()
      console.log(res)
    } catch (error) {
      console.log(error)
    }
  }

  const handleInputChange = (e: React.FormEvent<HTMLInputElement>) => {
    const { name, value } = e.currentTarget
    setInputValues(prevState => ({ ...prevState, [name]: value }))
  }

  return (
    <div className="Form">
      <div className="form-wrapper">
        <h1>Demo Form for React</h1>
        <form className="form">
          <input 
            className="form-input"
            name="name" 
            value={inputValues?.name || ''} 
            onChange={handleInputChange} 
            placeholder="Your Name"
            type="text"
            data-testid="form-input-name"
          />
          <input 
            className="form-input"
            name="phone" 
            value={inputValues?.phone || ''} 
            onChange={handleInputChange} 
            placeholder="Your Phone" 
            type="tel"
            data-testid="form-input-phone"
          />
          <input 
            className="form-input" 
            name="email"
            value={inputValues?.email || ''} 
            onChange={handleInputChange} 
            placeholder="Your Email" 
            type="email"
            data-testid="form-input-email"
          />
          <input 
            className="form-input"
            name="income"
            value={inputValues?.income || ''} 
            onChange={handleInputChange} 
            placeholder="Your Annual Income" 
            type="number"
            data-testid="form-input-income"
          />
          <button 
            className='form-submit'
            data-testid="form-submit" 
            onClick={handleFormSubmit}
          >
            Submit
          </button>
        </form>
      </div>
    </div>
  );
}

export default Form;

A sample Typescript form. It's features:

  • A single onChange handler
  • One state object into which we can add as many key value pairs without the typescript compiler yelling at us.
  • Uses ES2020 optional chaining.
  • Has data-testid on the DOM elements incase you want to run a few unit test.
  • Should provide autocomplete for the input fields as per their types.
  • A form Submit function sample that uses the fetch api to make a post call to an end point.

Also, with this approach, you don't have to use @ts-ignore, any or make changes to your tsconfig.

Please use it as your hearts desire.

1 Comment

Thank you, this is a much better answer than the top answer in my opinion!
0

In the interface you have to shape that value of your state which is [e.target.name] and e.target.value type string or any as per your code.

interface formState {
  //it can be  (name: string address: string)
  [key: string]: string;
}

handleChange(e: any) {
    this.setState({
      [e.target.name]: e.target.value
    });
  }

For example

interface LoginFormState {
      //can be
      //email: string
      //password: string
      [key: string]: string;
    }
    interface Props {}

    class LoginForm extends React.Component<Props, LoginFormState> {
      constructor(props: Props) {
        super(props);
        this.state = {
          email: "",
          password: ""
        };
        this.handleChange = this.handleChange.bind(this);
      }
      handleChange(e: any) {
        this.setState({
          [e.target.name]: e.target.value
        });
        // console.log("email", this.state.email);
      }

Hope this help

Comments

0

I think this approach is pretty simple to understand as well:

interface FormData {
    firstName?: string,
    lastName?: string,
}


const [form, setForm] = useState<FormData | null>(null);

And the JSX

<input onChange={(e) => setForm({...form, firstName: e.target.value})}  />
<input onChange={(e) => setForm({...form, lastName: e.target.value})}  />

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.