Обработка события onChange в TypeScript: как получить event.target.value
Правильное типизирование события onChange в React + TypeScript
Проблема, с которой вы столкнулись, очень распространена среди разработчиков, переходящих на TypeScript. Давайте разберёмся по шагам.
Почему возникает ошибка?
В TypeScript React.ChangeEvent — это обобщённый (generic) тип, который ожидает конкретный HTML-элемент. Когда вы пишете просто React.ChangeEvent, TypeScript не знает, с каким именно элементом вы работаете. По умолчанию event.target имеет тип EventTarget, у которого нет свойства value — оно есть только у конкретных элементов, например, HTMLInputElement.
Правильное решение
Используйте React.ChangeEvent:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return <input type="text" onChange={handleChange} />;
Теперь TypeScript знает, что e.target — это HTMLInputElement, у которого есть свойство value. Всё работает без ошибок.
Альтернативный подход (деструктуризация)
Можно сразу извлечь значение из события:
const handleChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
console.log(value);
};
Этот способ более лаконичный, но требует понимания деструктуризации.
Что делать, если элемент динамический?
Иногда вы не знаете заранее тип элемента (например, при работе с textarea или select). В таких случаях:
- Для
<textarea>—React.ChangeEvent<HTMLTextAreaElement> - Для
<select>—React.ChangeEvent<HTMLSelectElement> - Для универсального случая — используйте приведение типа:
(e.target as HTMLInputElement).value, но это менее предпочтительно.
Почему не работает приведение через as?
Некоторые разработчики пишут:
const handleChange = (e: React.ChangeEvent) => {
const value = (e.target as HTMLInputElement).value;
};
Это работает, но это "костыль". Вы теряете проверку типов — если передадите это событие для <div>, TypeScript не предупредит об ошибке. Используйте generic-типы — это правильный путь.
Полный пример с формой
import React, { useState } from 'react';
const MyForm: React.FC = () => {
const [name, setName] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Submitted:', name);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" value={name} onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
};
Теперь у вас есть полностью типизированная форма. TypeScript будет проверять, что вы используете правильные свойства и методы.
<<Использование обобщённой функции-обработчика
Если у вас много разных инпутов и вы не хотите писать отдельный обработчик для каждого, можно создать универсальную функцию с использованием generics:
const createChangeHandler = <T extends HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(
setter: React.Dispatch<React.SetStateAction<string>>
) => {
return (e: React.ChangeEvent<T>) => {
setter(e.target.value);
};
};
// Использование:
const [name, setName] = useState('');
const handleNameChange = createChangeHandler(setName);
return <input type="text" onChange={handleNameChange} />;
Работа с несколькими полями формы
Для форм с множеством полей удобно использовать один обработчик с name атрибутом:
interface FormState {
name: string;
email: string;
}
const [form, setForm] = useState<FormState>({ name: '', email: '' });
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
return (
<>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
</>
);
Этот подход масштабируется на любое количество полей и сохраняет строгую типизацию.