Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

Типобезопасные обработчики DOM-событий в React: практическое руководство по TypeScript

Тимофей Ищенко

Коротко: Руководство по типизации событий в 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:

  1. event.currentTarget — элемент, к которому непосредственно прикреплен обработчик. TypeScript строго типизирует его типом T, переданным в generic (например, HTMLInputElement).
  2. 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>.


Чек-лист по типизации событий для код-ревью

  1. Отсутствие any: параметры обработчиков типизированы явно или выведены компилятором.
  2. Использование типов React: применяются React.MouseEvent, React.ChangeEvent и другие интерфейсы из @types/react, а не глобальные DOM-типы.
  3. Указание generic-параметров: для событий указаны соответствующие HTML-элементы (HTMLButtonElement, HTMLInputElement, HTMLFormElement).
  4. Корректная работа с DOM-узлами: обращение к свойствам элемента выполняется через event.currentTarget.
  5. Композиция типов: переиспользуемые UI-компоненты расширяют стандартные атрибуты через React.ComponentPropsWithoutRef.

Применение этих правил исключает типичные ошибки во время выполнения, упрощает поддержку кода и делает архитектуру компонентов предсказуемой.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также