React
TypeScript и React
Компонент принимает проп user: any, и через месяц никто не помнит, есть ли у него поле email или emailAddress. Опечатка в имени пропса не ловится, событие onChange приходит как просто Event без target.value, а отрефакторить компонент страшно, потому что компилятор молчит. TypeScript превращает пропсы, состояние и события в проверяемый контракт: переименование поля ломает билд, автодополнение знает форму данных, а целый класс ошибок исчезает до запуска. При этом строгая дисциплина запрещает заглушки any, non-null assertion и касты - именно они тихо возвращают те же баги.
- Дизайн-системы и UI-киты, где пропсы компонента это публичный API, и его контракт обязан быть точным
- Крупные кодовые базы, где рефакторинг безопасен только потому, что компилятор ловит несовпадения типов
- Дженерик-компоненты вроде типизированной таблицы или списка, переиспользуемые с разными типами данных
- Формы и API-слой, где данные приходят как unknown и сужаются type guards, а не приводятся кастом
- Команды с запретом any, non-null assertion и as, где типобезопасность держится на дженериках и сужении
Предварительные знания
- Компоненты и пропсы, передача данных сверху вниз
- useState и обработчики событий в React
- Базовый TypeScript: типы, интерфейсы, объединения и дженерики на уровне идеи
Как TypeScript стал стандартом в React
Долгое время React типизировали через PropTypes - проверку пропсов в рантайме, которая ничего не давала на этапе компиляции. По мере роста кодовых баз стало ясно, что нужна статическая типизация. Типы для React в проекте DefinitelyTyped (@types/react) созрели к 2018 году, а Create React App и затем Next.js сделали TypeScript-шаблоны первоклассными. К середине 2020-х TypeScript стал фактическим стандартом: PropTypes в React 19 удалили из ядра, а строгий режим (strict) с запретом неявного any стал нормой для новых проектов.
Типизация пропсов, состояния и событий
Пропсы описываются типом или интерфейсом, и этот тип становится контрактом компонента. Опечатка в имени пропса, отсутствие обязательного значения или неверный тип ловятся на этапе компиляции. Состояние через useState чаще всего выводится из начального значения, а когда нужен union или объект сложнее, тип задаётся явным аргументом-дженериком.
События в React типизированы и несут точную форму. Обработчик ввода получает не абстрактный Event, а ChangeEvent для элемента input, поэтому event.target.value известен как строка без всяких проверок. Это убирает соблазн привести событие кастом, чтобы добраться до полей.
Тип props: any возвращает ровно те баги, ради которых берут TypeScript: опечатки в полях и неверные значения снова проходят молча. any это не типизация, а её отключение. Если форма данных пока неизвестна, честнее unknown с последующим сужением, а не any.
Почему типизировать пропсы как any это плохой выбор?
Дженерик-компоненты, ReactNode и ReactElement
Дженерик-компонент параметризуется типом данных и сохраняет связь между входом и выходом. Типизированный список принимает массив элементов типа T и функцию отрисовки, которая получает ровно T. Так один компонент переиспользуется с пользователями, заказами и чем угодно, не теряя точных типов и не прибегая к any.
В типах часто встречаются ReactNode и ReactElement, и их путают. ReactNode это всё, что React способен отрендерить: элемент, строка, число, массив таких значений, а также null и undefined. ReactElement это конкретный объект-элемент, который возвращает createElement или JSX. Для пропа children почти всегда нужен именно ReactNode, потому что туда передают самое разное.
| Тип | Что охватывает | Когда брать |
|---|---|---|
| ReactNode | Элементы, строки, числа, массивы, null, undefined | Тип для children и любого рендеримого контента |
| ReactElement | Конкретный объект-элемент от JSX или createElement | Когда нужен именно один элемент, например для cloneElement |
| JSX.Element | Практически синоним ReactElement в JSX | Возвращаемое значение компонента (часто выводится само) |
Для типа children в большинстве компонентов берут children: React.ReactNode. ReactElement стоит требовать лишь там, где код реально работает с одиночным элементом - например, клонирует его через cloneElement и добавляет пропсы.
В чём разница между ReactNode и ReactElement?
Дискриминированные объединения и сужение без кастов
Иногда наборы пропсов взаимоисключающие: у кнопки-ссылки обязателен href, а у кнопки-действия onClick, и смешивать их нельзя. Один плоский тип с необязательными полями этого не выражает: href и onClick оба становятся опциональными, и компилятор допускает бессмысленные комбинации. Дискриминированное объединение задаёт варианты с общим полем-меткой, по которому тип сужается.
Проверка props.as === 'link' сужает тип внутри ветки, и компилятор сам знает, какие поля доступны. Это и есть type guard - проверка значения, после которой тип уточняется без всякого приведения. Невозможная комбинация (href вместе с onClick) теперь просто не выражается типом, а не отлавливается в рантайме.
Соблазн написать data as User или value!.name даёт ложную уверенность: компилятор замолкает, но в рантайме поле может оказаться undefined. Type guard isUser проверяет данные по-настоящему и сужает тип честно. Это и есть правильная замена кастам и non-null assertion.
Зачем для взаимоисключающих наборов пропсов использовать дискриминированное объединение, а не один тип с опциональными полями?
Связь с другими темами
Этот урок про типизацию React. Рядом стоят соседние темы:
- Компоненты и пропсы — Базовая модель, для которой типизация пропсов задаёт проверяемый контракт
- Формы с RHF и Zod — Прикладной пример строгой типизации: тип формы выводится из схемы, данные сужаются без any и кастов
Итог
- Пропсы, состояние и события типизируются явно: переименование поля ломает билд, а автодополнение знает форму данных
- Дженерик-компоненты переиспользуются с разными типами данных, сохраняя связь типов входа и выхода
- ReactNode это всё, что можно отрендерить (узлы, строки, массивы, null), а ReactElement это конкретный элемент от createElement
- Дискриминированные объединения задают взаимоисключающие наборы пропсов, делая невозможные комбинации невыразимыми
- Строгие правила: никаких any, non-null assertion и явных кастов - вместо них дженерики, type guards и сужение типов
- Данные неизвестной формы типизируются как unknown и сужаются проверками, а не приводятся через as к желаемому типу
Связанные уроки
- rc-03-components-props — Типизация пропсов это прямое продолжение модели компонентов и пропсов
- rc-39-forms-zod — Вывод типа формы из Zod-схемы это прикладной случай строгой типизации без any и кастов