Коротко: Руководство по типизации событий в React с TypeScript: SyntheticEvent, дженерики DOM-элементов, currentTarget, устранение any и типизация UI-компонентов.
При переходе на TypeScript разработчики часто сталкиваются с ошибками типизации при обработке пользовательского ввода: компилятор сообщает об отсутствии поля value у event.target, ругается на несовместимость сигнатур в onClick или провоцирует использование небезопасных конструкций вроде (e: any).
Понимание того, как пакет @types/react описывает синтетические события и как устроены generic-параметры элементов, позволяет исключить any, сократить технический долг и сделать поведение интерфейсных компонентов предсказуемым.
Анатомия событий в React: SyntheticEvent против нативного DOM Event
React не привязывает обработчики напрямую к узлам DOM для каждого элемента JSX. Вместо этого библиотека использует собственную кроссбраузерную обертку — SyntheticEvent. Она сглаживает различия в поведении браузеров и инкапсулирует нативный объект события.
Почему нельзя использовать глобальные типы DOM
В глобальной области видимости TypeScript уже объявлены типы браузерного DOM API: MouseEvent, KeyboardEvent, Event. Если передать в JSX обработчик с нативным типом:
// Ошибка: нативный MouseEvent не совпадает с типом обработчика React
const handleClick = (e: MouseEvent) => {
console.log(e.clientX);
};
React выдаст ошибку несовместимости типов в JSX-атрибуте onClick. Все события в компонентах должны использовать интерфейсы из пространства имен React (например, React.MouseEvent или базовый React.SyntheticEvent).
Иерархия синтетических типов
Базовым интерфейсом для всех событий является React.SyntheticEvent<T, E>. От него наследуются специализированные интерфейсы:
React.MouseEvent<T>— клики, наведение, координаты курсора (onClick,onMouseDown,onMouseEnter);React.ChangeEvent<T>— изменение значений элементов формы (<input>,<textarea>,<select>);React.FormEvent<T>— отправка и валидация форм (onSubmit,onReset);React.KeyboardEvent<T>— нажатия клавиш на клавиатуре (onKeyDown,onKeyUp);React.FocusEvent<T>— управление фокусом (onFocus,onBlur);React.PointerEvent<T>— унифицированные события указателя (мышь, перо, сенсорный экран);React.DragEvent<T>— события перетаскивания элементов (Drag and Drop).
Параметр T в каждом из этих типов указывает на конкретный тип HTML-элемента, к которому привязан слушатель.
Основные типы событий и применение Generic-параметров
Большинство интерфейсов событий в React принимают generic-тип, указывающий на целевой DOM-узел. Это обеспечивает строгую типизацию свойств, привязанных к элементу.
Клики и мышь: React.MouseEvent<T>
Для кнопок, ссылок и интерактивных блоков используется React.MouseEvent:
import React from 'react';
export const ActionButton = () => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
// Доступны координаты клика и свойства целевой кнопки
console.log(`Кнопка мыши: ${event.button}`);
console.log(`ID кнопки: ${event.currentTarget.id}`);
};
return (
<button id="submit-btn" type="button" onClick={handleClick}>
Выполнить
</button>
);
};
Поля ввода: React.ChangeEvent<T>
Событие onChange в React срабатывает при каждом изменении содержимого поля (в отличие от нативного DOM-события change, которое вызывается при потере фокуса).
import React, { useState } from 'react';
export const TextInput = () => {
const [text, setText] = useState('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.currentTarget.value);
};
return <input type="text" value={text} onChange={handleChange} />;
};
Для выпадающих списков и многострочных текстовых полей тип элемента меняется соответственно:
React.ChangeEvent<HTMLSelectElement>React.ChangeEvent<HTMLTextAreaElement>
Отправка формы: React.FormEvent<T>
Для события onSubmit на теге <form> используется React.FormEvent<HTMLFormElement>:
import React from 'react';
export const FeedbackForm = () => {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
console.log(Object.fromEntries(formData));
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<button type="submit">Отправить</button>
</form>
);
};
Клавиатура и фокус: React.KeyboardEvent<T> и React.FocusEvent<T>
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
console.log('Подтвержден ввод в поле:', event.currentTarget.name);
}
};
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
console.log('Поле потеряло фокус со значением:', event.currentTarget.value);
};
event.currentTarget против event.target: безопасный доступ к свойствам
Частая ошибка типизации возникает при попытке прочитать event.target.value: компилятор сообщает, что свойство value отсутствует у типа EventTarget.
Разница обусловлена механизмом всплытия событий (Event Bubbling) и тем, как типизированы эти свойства в @types/react:
event.currentTarget— элемент, к которому непосредственно прикреплен обработчик. TypeScript строго типизирует его типомT, переданным в generic (например,HTMLInputElement).event.target— элемент, на котором фактически произошло событие (наиболее глубокий потомок в DOM-дереве). TypeScript типизирует его как общийEventTarget, поскольку во время компиляции невозможно однозначно определить, какой дочерний узел инициировал всплытие.
import React from 'react';
export const ActionCard = () => {
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
// currentTarget гарантированно является HTMLDivElement
console.log('ID контейнера:', event.currentTarget.id);
// event.target — это EventTarget, у него нет полей id или tagName без сужения
if (event.target instanceof HTMLElement) {
console.log('Элемент, по которому кликнули:', event.target.tagName);
}
};
return (
<div id="card-container" onClick={handleClick}>
<h3>Карточка</h3>
<button type="button">Вложенная кнопка</button>
</div>
);
};
Практическое правило: для чтения значений и атрибутов элемента, на котором зарегистрирован обработчик, всегда используйте event.currentTarget.
Два подхода к объявлению функций-обработчиков
Существует два равноправных способа типизации вынесенных функций.
Подход 1: Типизация аргумента события (рекомендуемый)
Тип события указывается непосредственно в параметре функции. Это наиболее распространенный и наглядный паттерн:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
console.log(e.currentTarget.value);
};
Подход 2: Использование функционального типа
Пакет @types/react предоставляет готовые функциональные типы с суффиксом Handler: MouseEventHandler<T>, ChangeEventHandler<T>, FormEventHandler<T>:
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
// Параметр e автоматически получает тип React.ChangeEvent<HTMLInputElement>
console.log(e.currentTarget.value);
};
Оба варианта обеспечивают полную типобезопасность. Первый подход чаще выбирают за явный контроль возвращаемого значения и удобство при деструктуризации аргументов.
Автоматический вывод типов в inline-функциях
Если обработчик объявляется непосредственно в JSX, TypeScript выводит тип события автоматически за счет контекстной типизации (Contextual Typing). Дополнительные аннотации здесь не требуются:
// TypeScript автоматически определяет, что e — это React.ChangeEvent<HTMLInputElement>
<input onChange={(e) => console.log(e.currentTarget.value)} />
Типизация пропсов в переиспользуемых компонентах
При проектировании дизайн-систем и UI-китов важно правильно описывать интерфейсы пропсов, принимающих внешние колбэки.
Явное объявление сигнатуры
Если компонент принимает обработчик для конкретного действия:
interface CustomButtonProps {
label: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
}
export const CustomButton = ({ label, onClick, disabled }: CustomButtonProps) => {
return (
<button type="button" onClick={onClick} disabled={disabled} className="btn-primary">
{label}
</button>
);
};
Наследование стандартных HTML-атрибутов
Чтобы не описывать вручную каждое нативное событие (onClick, onKeyDown, onFocus), используйте утилиту React.ComponentPropsWithoutRef:
import React from 'react';
interface BaseInputProps extends React.ComponentPropsWithoutRef<'input'> {
label: string;
errorMessage?: string;
}
export const BaseInput = ({ label, errorMessage, ...restProps }: BaseInputProps) => {
return (
<div className="input-wrapper">
<label>{label}</label>
{/* Все обработчики (onChange, onBlur, onFocus) типизированы автоматически */}
<input {...restProps} />
{errorMessage && <span className="error-text">{errorMessage}</span>}
</div>
);
};
Распространенные антипаттерны
1. Использование типа any в параметрах обработчика
// Антипаттерн: отключается проверка типов, отсутствует автодополнение в IDE
const handleInput = (e: any) => {
setValue(e.target.val); // Опечатка 'val' вместо 'value' пройдет компиляцию
};
// Корректно: строгая типизация и защита от опечаток
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.currentTarget.value);
};
2. Принудительное приведение типов через as
Попытка обойти ошибку target с помощью небезопасного каста:
// Антипаттерн: приведет к ошибке во время выполнения, если клик был по дочернему элементу
const handleClick = (e: React.MouseEvent) => {
const val = (e.target as HTMLInputElement).value;
};
// Корректно: доступ через строго типизированный currentTarget
const handleClick = (e: React.MouseEvent<HTMLInputElement>) => {
const val = e.currentTarget.value;
};
3. Неправильный выбор интерфейса события
Использование React.MouseEvent для формы вместо React.FormEvent делает невозможным перехват отправки через клавиатуру (нажатие Enter) и лишает доступа к методам управления отправкой.
Вопросы и ответы (FAQ)
В чем разница между React.MouseEvent<HTMLButtonElement> и React.MouseEventHandler<HTMLButtonElement>?
React.MouseEvent<HTMLButtonElement> — это тип объекта события, передаваемого в функцию первым параметром. React.MouseEventHandler<HTMLButtonElement> — это тип всей функции-обработчика: (event: React.MouseEvent<HTMLButtonElement>) => void.
Почему возникает ошибка при попытке прочитать event.target.value?
Свойство event.target описывает инициатора события в DOM-дереве и типизировано как общий интерфейс EventTarget, где нет поля value. Для безопасного доступа к значению элемента используйте event.currentTarget.value.
Нужно ли типизировать события в стрелочных функциях внутри JSX?
Нет, при написании inline-функций TypeScript автоматически выводит тип аргумента из контекста JSX-атрибута.
Какой тип использовать для обработки отправки формы?
Используйте React.FormEvent<HTMLFormElement>. Этот тип предоставляет доступ к вызову event.preventDefault() и элементам формы через event.currentTarget.
Как типизировать обработчик, привязанный одновременно к <input> и <textarea>?
Используйте объединение типов (Union Type) в generic-параметре: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>.
Чек-лист по типизации событий для код-ревью
- Отсутствие
any: параметры обработчиков типизированы явно или выведены компилятором. - Использование типов React: применяются
React.MouseEvent,React.ChangeEventи другие интерфейсы из@types/react, а не глобальные DOM-типы. - Указание generic-параметров: для событий указаны соответствующие HTML-элементы (
HTMLButtonElement,HTMLInputElement,HTMLFormElement). - Корректная работа с DOM-узлами: обращение к свойствам элемента выполняется через
event.currentTarget. - Композиция типов: переиспользуемые UI-компоненты расширяют стандартные атрибуты через
React.ComponentPropsWithoutRef.
Применение этих правил исключает типичные ошибки во время выполнения, упрощает поддержку кода и делает архитектуру компонентов предсказуемой.




.svg.webp)




