TypeScript + React.js: практическая разработка фронтенда

Курс посвящён использованию TypeScript в React-приложениях: от базовой типизации до архитектуры компонентов и работы с данными. Вы научитесь проектировать типы, писать безопасные компоненты, типизировать хуки и интеграции с популярными библиотеками.

1. Введение: настройка TypeScript в React и инструменты

Введение: настройка TypeScript в React и инструменты

Зачем TypeScript в React-проекте

TypeScript добавляет статическую типизацию в JavaScript: вы описываете, какие данные ожидаете (например, что age — число), и получаете проверки ещё до запуска приложения. В React это особенно полезно, потому что типы помогают:

  • Надёжнее работать с props и state
  • Безопаснее обрабатывать события (клики, ввод в input)
  • Быстрее рефакторить код благодаря подсказкам IDE
  • Ловить ошибки на этапе разработки, а не у пользователей
  • Что мы настроим в этой статье

  • Создадим новый проект React + TypeScript
  • Разберём, как устроена сборка и где участвует TypeScript
  • Настроим базовые инструменты качества: линтер и форматирование
  • Подготовим удобную среду разработки
  • Требования к окружению

    Перед началом установите:

  • Node.js
  • Редактор Visual Studio Code
  • Менеджер пакетов можно выбрать любой:

    | Менеджер | Когда выбирать | |---|---| | npm | Уже установлен вместе с Node.js, достаточно для большинства задач | | pnpm | Быстрее и экономит место на диске за счёт переиспользования пакетов (pnpm) | | yarn | Популярен в командах, где он уже принят как стандарт (Yarn) |

    Дальше в примерах будет npm, но команды легко адаптируются.

    Создаём React + TypeScript проект на Vite

    Для современного старта удобно использовать Vite: он быстро запускает dev-сервер и хорошо работает с React.

    Официальная документация: Vite

  • Создайте проект:
  • Перейдите в папку проекта и установите зависимости:
  • Запустите проект:
  • Откройте адрес, который покажет терминал (обычно http://localhost:5173).

    Как здесь участвует TypeScript

    Важно понимать роли инструментов:

  • Транспиляция — превращение кода одного вида в другой. Например, TypeScript-код превращается в JavaScript.
  • Бандлер — инструмент, который собирает множество файлов (модули, стили, картинки) в набор файлов, удобных для браузера.
  • Type-checking — проверка типов TypeScript (совместимы ли данные с описанными типами).
  • В связке Vite + TypeScript обычно происходит так:

  • Vite быстро обрабатывает исходники (для скорости он ориентируется на быстрые трансформеры)
  • Проверка типов TypeScript чаще всего запускается отдельно командой tsc
  • !Схема показывает, что тип-проверка (tsc) — отдельный шаг, а Vite отвечает за запуск и сборку

    Структура проекта и ключевые файлы

    После создания проекта вы увидите знакомые элементы:

  • src/ — исходный код приложения
  • src/main.tsx — точка входа (инициализация React в браузере)
  • src/App.tsx — пример компонента
  • tsconfig.json — настройки TypeScript
  • vite.config.ts — настройки Vite
  • package.json — зависимости и скрипты
  • Файлы с React-компонентами обычно имеют расширение *.tsx:

  • *.ts — TypeScript без JSX
  • *.tsx — TypeScript с JSX (разметкой внутри кода)
  • JSX — синтаксис, похожий на HTML, который React использует для описания интерфейса.

    Базовая настройка TypeScript: tsconfig.json

    TypeScript управляется через tsconfig.json. Не нужно пытаться выучить все опции сразу — достаточно понимать основные группы.

    Что важно в начале

  • strict — включает строгие проверки типов. Для обучения и реальных проектов это почти всегда хорошая идея: ошибок меньше на проде.
  • jsx — режим обработки JSX для React.
  • noEmit — запрет на генерацию файлов при проверке типов. Это удобно, когда сборкой занимается Vite, а tsc используется только как проверка.
  • Документация: TSConfig Reference

    Рекомендуемый скрипт для проверки типов

    Добавьте в package.json (в секцию scripts) команду:

    И запускайте:

    Это дисциплинирует: проект может собираться, но типы при этом могут быть некорректны — отдельная проверка помогает не пропустить проблему.

    Типы React: откуда они берутся

    React и браузерные API уже имеют готовые типы:

  • Типы React поставляются вместе с экосистемой React (в современных шаблонах они уже установлены)
  • Типы DOM поставляются с TypeScript (это часть стандартных библиотек)
  • Если вы когда-нибудь увидите ошибки вида Cannot find name JSX или проблемы с типами React-компонентов, проверьте, что зависимости React установлены корректно, а проект действительно создан как React + TS шаблон.

    Справочник по TypeScript: TypeScript Handbook

    Инструменты качества кода: ESLint и Prettier

    Чтобы код в команде был единообразным и безопасным:

  • ESLint (линтер) ищет потенциальные ошибки и плохие практики
  • Prettier (форматтер) приводит код к единому стилю
  • Документация:

  • ESLint
  • Prettier
  • typescript-eslint
  • Установка и базовая интеграция

    Установите пакеты:

    Затем:

  • Настройте ESLint-конфиг (формат зависит от выбранного шаблона и версии ESLint)
  • Подключите eslint-config-prettier, чтобы правила ESLint не конфликтовали с форматированием Prettier
  • Если в вашем шаблоне Vite ESLint уже есть, добавьте только недостающие зависимости и правила.

    Полезные скрипты

    Добавьте в package.json:

    Запуск:

    Настройка VS Code для TypeScript + React

    VS Code умеет работать с TypeScript почти из коробки, но стоит включить несколько удобств:

  • Установите расширение ESLint
  • Установите расширение Prettier - Code formatter
  • Рекомендуемые привычки разработки:

  • Форматирование при сохранении (Format on Save)
  • Исправление простых проблем ESLint автоматически при сохранении (если настроено)
  • Регулярный запуск npm run typecheck перед коммитом
  • Мини-чеклист готового проекта

  • Проект создаётся и запускается через npm run dev
  • Компоненты пишутся в *.tsx
  • Есть отдельная команда npm run typecheck
  • Линтинг и форматирование запускаются отдельными командами
  • VS Code показывает ошибки типов и подсказки по коду
  • Что будет дальше в курсе

    В следующих материалах мы начнём писать реальные компоненты на TypeScript и разберём:

  • Типизацию props и композицию компонентов
  • События и формы в React с TypeScript
  • Типизацию состояния и работы с данными
  • Практики организации типов в проекте
  • 2. Типы и интерфейсы TypeScript для фронтенда

    Типы и интерфейсы TypeScript для фронтенда

    Связь с предыдущей статьёй

    В прошлой статье мы настроили проект React + TypeScript на Vite, добавили отдельную команду для проверки типов (npm run typecheck) и подключили инструменты качества кода. Теперь можно переходить к самому важному практическому навыку: как описывать данные в TypeScript так, чтобы React-компоненты, события и ответы API были типобезопасными.

    Полезные справочники:

  • TypeScript Handbook
  • Everyday Types
  • React TypeScript Cheatsheets
  • Что такое тип в TypeScript

    Тип описывает, какие значения допустимы.

  • string означает, что значение обязано быть строкой
  • number означает число
  • boolean означает true или false
  • TypeScript проверяет согласованность типов во время разработки. Это особенно полезно во фронтенде, где данные приходят из форм, URL, localStorage, API и часто имеют непредсказуемую форму.

    Базовые типы, которые постоянно встречаются во фронтенде

  • string, number, boolean — примитивы
  • null, undefined — отсутствие значения
  • unknown — значение неизвестного типа (безопаснее, чем any)
  • any — отключает проверку типов (в реальном проекте лучше избегать)
  • never — ситуация, которая невозможна (например, функция, которая всегда выбрасывает ошибку)
  • Пример: значение из API лучше начинать как unknown, а не как any, и затем проверять.

    Объектные типы: описание формы данных

    Чаще всего во фронтенде вы описываете объекты: пользователя, товар, настройки, ответ сервера.

  • id: string — поле обязательно
  • age?: number — поле опционально, может отсутствовать
  • Опциональность полезна, но её легко перепутать с age: number | undefined.

  • age?: number означает, что поля может не быть вообще
  • age: number | undefined означает, что поле есть, но значение может быть undefined
  • Массивы и кортежи

    Массивы во фронтенде встречаются повсюду: список товаров, сообщения чата, результаты поиска.

    Кортеж — это массив фиксированной длины, где важны позиции.

    Кортежи полезны, например, для пар значений key-value, координат, результатов парсинга.

    Объединения: когда значение может быть одним из нескольких типов

    Объединение (union) записывается через |.

    Это типичный фронтенд-кейс: состояние загрузки.

    Объединения отлично работают вместе с сужением типов через проверки.

    Литеральные типы: ограничения на конкретные значения

    Литеральные типы помогают жёстко ограничить набор значений.

    Такой подход в UI-компонентах даёт большие преимущества:

  • IDE подсказывает допустимые варианты
  • нельзя случайно передать "primry"
  • Пересечения: объединяем несколько типов в один

    Пересечение (intersection) записывается через & и означает: объект должен удовлетворять всем частям.

    !Диаграмма показывает, что пересечение типов объединяет требования двух объектных типов

    Type alias и interface: что выбрать

    В TypeScript есть два основных способа описывать форму объекта: type и interface.

    Когда удобно type

    type универсален: им можно описывать не только объекты.

  • объединения: A | B
  • пересечения: A & B
  • литералы: "a" | "b"
  • примитивы и алиасы: type Id = string
  • Когда удобно interface

    interface традиционно используют для публичных контрактов и объектных моделей.

    Плюсы interface, полезные в больших фронтенд-проектах:

  • более естественное расширение через extends
  • поддержка declaration merging (когда одноимённые интерфейсы могут объединяться)
  • Практическое правило для курса:

  • для props компонентов часто удобно type, потому что там регулярно встречаются объединения и композиция
  • для моделей домена (например, User, Product) подойдут и type, и interface, важно выбрать один стиль и придерживаться его в проекте
  • Документация по различиям: TypeScript: Interfaces vs. Type Aliases

    Типизация функций: параметры и возвращаемые значения

    Функции — это обработчики событий, преобразователи данных, утилиты для форматирования.

    В UI это помогает гарантировать, что форматтер всегда принимает число и возвращает строку.

    Дженерики: типы, зависящие от других типов

    Дженерики нужны, когда вы пишете переиспользуемую логику: например, обёртку над ответом API.

    Использование:

    Так вы получаете типобезопасную основу для работы с сервером.

    Сужение типов: как безопасно работать с union

    Если тип — объединение, TypeScript требует проверить, какая ветка используется.

    Здесь r.okдискриминатор: поле, по которому TypeScript понимает, какая ветка union активна.

    Utility Types: быстрые преобразования типов

    TypeScript даёт набор готовых утилит, которые постоянно используются во фронтенде.

    Документация: Utility Types

    Самые практичные:

  • Partial<T> — делает все поля опциональными (удобно для форм редактирования)
  • Pick<T, K> — выбрать часть полей (DTO, UI-модель)
  • Omit<T, K> — исключить поля (например, убрать id при создании)
  • Record<K, V> — объект-словарь
  • Примеры:

    Record часто используется для маппинга статусов и текстов.

    Как это применяется в React уже сейчас

    Типизация props

    Что это даёт:

  • нельзя забыть alt
  • нельзя передать size="xl"
  • Типизация обработчиков событий

    Во фронтенде очень важно корректно типизировать события ввода.

    Тип React.ChangeEventHandler<HTMLInputElement> гарантирует, что e.target.value существует и является строкой.

    Практические советы по организации типов в проекте

  • описывайте модели данных рядом с местом использования, пока они маленькие
  • когда типы начинают переиспользоваться, выносите их в src/types/ или в папку конкретного модуля, например src/features/user/types.ts
  • не используйте any, если можно начать с unknown и аккуратно проверить форму данных
  • используйте литеральные типы для UI-вариантов, статусов и режимов отображения
  • Что дальше

    Дальше в курсе мы будем расширять практику типизации в React:

  • типизация props сложных компонентов и композиции
  • типобезопасная работа с формами и валидацией
  • типизация состояния, асинхронных запросов и данных API
  • С этого момента полезная привычка: при каждом изменении контрактов данных запускать npm run typecheck, чтобы ловить проблемы до запуска приложения.

    3. Типизация React-компонентов: props, state и children

    Типизация React-компонентов: props, state и children

    Связь с предыдущими материалами

    В первой статье мы настроили проект React + TypeScript и инструменты проверки качества. Во второй — разобрали базовые типы, объединения, дженерики и utility types, которые чаще всего нужны во фронтенде.

    Теперь соберём это в практику: научимся типизировать React-компоненты так, чтобы:

  • props были строгими и самодокументируемыми
  • state не превращался в any и корректно отражал возможные состояния UI
  • children поддерживали как обычную разметку, так и продвинутые паттерны вроде render props
  • Полезные источники:

  • React TypeScript Cheatsheets
  • Документация React: useState
  • Документация React: useReducer
  • TypeScript Handbook: Everyday Types
  • !Визуально показывает, что state живёт в компоненте, вниз передаются props и children, а наверх события возвращаются через колбэки

    Типизация props: контракт компонента

    Props — это входные данные компонента. В TypeScript вы описываете их типом, и затем получаете:

  • подсказки IDE при использовании компонента
  • ошибки компиляции при неправильных значениях
  • безопасный рефакторинг
  • Базовая типизация props

    Ключевые моменты:

  • disabled?: boolean означает, что проп может отсутствовать
  • значение по умолчанию (disabled = false) лучше задавать прямо в параметрах функции
  • литеральные типы для variant ограничивают допустимые варианты и защищают от опечаток
  • Передача данных и колбэков

    Один из самых полезных паттернов React — передавать вниз данные, а вверх события через колбэк-проп.

    Здесь важно типизировать колбэк как функцию с понятными параметрами, а не как Function.

    Деструктуризация и типы

    Обычно тип указывается на весь объект props, а не на каждое поле отдельно.

    Так проще менять контракт компонента: вы редактируете тип в одном месте.

    Типизация children: что можно вкладывать внутрь компонента

    children — это содержимое между открывающим и закрывающим тегом компонента.

    Самый практичный тип: React.ReactNode

    React.ReactNode покрывает почти все варианты:

  • текст
  • JSX
  • массив JSX
  • null и false (часто используются для условного рендера)
  • Если children необязателен, делайте его опциональным:

    Когда children должен быть render prop

    Иногда children — это функция, которая возвращает разметку на основе данных.

    Здесь важно явно типизировать сигнатуру children, иначе легко потерять подсказки по параметрам.

    Какой тип компонента использовать: обычная функция или React.FC

    В TypeScript вы чаще всего будете писать компоненты как обычные функции.

    Почему это хороший дефолт:

  • меньше “магии” и скрытых типов
  • проще контролировать, есть ли children и какого он типа
  • удобнее работать с дженериками в компонентах
  • React.FC встречается в коде, но его использование не обязательно и часто не даёт преимуществ в современных проектах. Важно понимать главное: тип компонента задаётся через типизацию props, а не через выбор React.FC.

    Типизация state: useState без ловушек

    state — это изменяемые данные внутри компонента. TypeScript может вывести тип автоматически, но в некоторых случаях ему нужна помощь.

    Примитивы: тип выводится автоматически

    useState(0) даёт тип number, и дополнительная аннотация обычно не нужна.

    Объекты и массивы: задавайте тип, если начальное значение пустое

    Проблема возникает, когда начальное значение не подсказывает структуру.

    Здесь useState<Todo[]>([]) важен: иначе массив будет “слишком общим”, и типизация дальнейших операций станет хуже.

    null в state: используйте union

    Частая ситуация во фронтенде: данные ещё не загружены.

    Union User | null заставляет вас обработать состояние загрузки, а не надеяться, что user “точно уже есть”.

    Статусы UI: литеральные объединения

    Вместо “магических строк” лучше завести тип статуса.

    Плюс такого подхода: вы не сможете случайно записать "lodading".

    Ленивое начальное значение

    Если начальное значение вычисляется дорого или зависит от внешнего источника, передавайте функцию.

    Тип здесь тоже выводится корректно, а вычисление произойдёт только один раз.

    Когда useReducer лучше useState: типобезопасные переходы состояния

    Если state сложный и меняется по множеству событий, удобнее useReducer. Ключевой плюс в TypeScript — типизация actions через дискриминирующее объединение.

    Что даёт TypeScript:

  • нельзя отправить экшен с несуществующим type
  • нельзя забыть payload там, где он обязателен
  • внутри switch автоматически доступен правильный тип action для каждой ветки
  • Продвинутая типизация props: разделение вариантов через union

    Иногда компонент имеет несколько режимов, и набор допустимых пропов зависит от режима. Это типичный кейс для дискриминирующих объединений.

    Теперь невозможно случайно передать kind: "number" вместе со строковым value.

    Практические правила для проекта

  • Типизируйте props через отдельный type или interface, который читается как контракт компонента.
  • Для children по умолчанию используйте React.ReactNode; если children — функция, типизируйте сигнатуру явно.
  • В useState:
  • - не добавляйте тип без необходимости, если тип выводится из начального значения - добавляйте тип, если начальное значение пустое ([], {}) или null
  • Рассматривайте useReducer, когда появляется много вариантов изменения state: actions с union часто делают код проще и безопаснее.
  • Что дальше

    Следующий шаг — научиться типизировать пользовательский ввод и формы в React: события, значения полей, валидацию, а также связывать это с моделями данных и ответами API. На этом фундаменте вы сможете строить компоненты, которые сложно “сломать” неправильными данными.

    4. Хуки в TypeScript: useState, useReducer, useRef, useMemo

    Хуки в TypeScript: useState, useReducer, useRef, useMemo

    Связь с предыдущими статьями

    Ранее мы:

  • настроили React + TypeScript проект и отдельную проверку типов
  • разобрали типы, объединения, дженерики и utility types
  • научились типизировать компоненты, props, state и children
  • Теперь закрепим практику на хуках React, которые чаще всего используются в реальных приложениях. Цель — писать код, где состояние, переходы состояния, ссылки на DOM и вычисляемые значения остаются типобезопасными и предсказуемыми.

    Полезные ссылки:

  • useState
  • useReducer
  • useRef
  • useMemo
  • TypeScript Handbook
  • !Схема показывает, какую задачу решает каждый хук и как он связан с рендером

    Главная идея типизации хуков

    Типизация в хуках решает две задачи:

  • корректность данных: вы не присваиваете в состояние то, чего там быть не может
  • корректность сценариев UI: вы вынуждены обработать варианты вроде null, статусов загрузки или разных типов действий
  • Практическое правило: TypeScript должен отражать реальные состояния интерфейса, а не только текущую ветку кода.

    useState в TypeScript

    useState хранит локальное состояние компонента. В большинстве случаев TypeScript выводит тип из начального значения, но есть ситуации, где аннотация обязательна.

    Когда тип выводится сам

    Здесь тип countnumber.

    Пустой массив или пустой объект

    Если начальное значение [], TypeScript не узнает тип элементов.

    Состояние с null: обязателен union

    Это типичный кейс для данных, которые загружаются.

    Важно: union User | null заставляет вас обработать состояние, когда пользователя ещё нет.

    Состояния UI как литеральные объединения

    Ленивое начальное значение

    Если значение читается из localStorage или вычисляется дорого, передавайте функцию.

    useReducer в TypeScript

    useReducer удобен, когда:

  • состояние состоит из нескольких полей
  • есть много разных событий, меняющих состояние
  • важны строгие переходы состояния
  • Сильная сторона TypeScript здесь — дискриминирующее объединение для действий.

    Базовый пример: actions как union

    Здесь TypeScript гарантирует:

  • нельзя отправить dispatch({ type: "xxx" })
  • нельзя забыть payload для type: "add"
  • Полезный приём: исчерпывающая проверка действий

    Если вы добавили новый action и забыли обработать его в reducer, удобно упасть в ошибку ещё на этапе типизации.

    Если вы добавите { type: "toggle" } в Action, TypeScript заставит обработать его в switch.

    useRef в TypeScript

    useRef используется в двух разных смыслах:

  • ссылка на DOM-элемент
  • мутабельное значение, которое живёт между рендерами, но не вызывает перерендер
  • DOM ref: почти всегда T | null

    При первом рендере элемента ещё нет, поэтому current будет null.

    Ключевые моменты:

  • тип HTMLInputElement | null отражает реальность
  • безопасный доступ через опциональную цепочку ?.
  • Мутабельное значение без перерендера

    Это полезно для хранения, например, id таймера, последнего значения, счётчика рендеров.

    Важно понимать: изменение renders.current не вызывает перерендер.

    Типовая ошибка: забыть про null

    Если написать useRef<HTMLInputElement>(null), TypeScript будет ругаться, потому что null не является HTMLInputElement. Правильнее — HTMLInputElement | null.

    useMemo в TypeScript

    useMemo кэширует результат вычисления между рендерами. Он полезен, когда:

  • вычисление заметно тяжёлое
  • вы хотите сохранить ссылочную стабильность результата, чтобы не триггерить лишние эффекты или ререндеры дочерних компонентов
  • Ссылка на документацию: useMemo

    Тип обычно выводится автоматически

    Тип filtered здесь будет Product[].

    Зачем следить за зависимостями

    useMemo пересчитывает значение, если поменялась любая зависимость из массива. Это означает:

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

  • вычисляйте в useMemo только то, что действительно зависит от указанных переменных
  • не используйте useMemo для того, чтобы починить неправильную архитектуру данных
  • useMemo не заменяет состояние

    useMemo — это производное значение от текущих props и state.

  • если значение должно меняться только через события пользователя и хранить историю — это useState или useReducer
  • если значение полностью вычисляется из других данных — это кандидат на useMemo или просто вычисление в рендере
  • Краткая таблица: когда какой хук

    | Хук | Что хранит | Вызывает перерендер при изменении | Типичный кейс | |---|---|---|---| | useState<T> | локальное значение T | да | формы, переключатели, статусы | | useReducer | состояние + правила переходов | да | сложная логика UI, много событий | | useRef<T> | current: T между рендерами | нет | DOM-ссылки, таймеры, счётчики | | useMemo<T> | кэш результата T | нет, но влияет на рендер | тяжёлые вычисления, стабильные ссылки |

    Практические правила, которые экономят время

  • В useState добавляйте дженерик только там, где TypeScript не может вывести тип или вам нужен union.
  • Для useReducer задавайте Action как дискриминирующее объединение и по возможности делайте исчерпывающую проверку.
  • Для DOM-ссылок в useRef используйте T | null и безопасный доступ ?..
  • useMemo применяйте осознанно: он оптимизирует производительность, но усложняет код и может скрыть ошибки зависимостей.
  • Что дальше

    Следующий практический шаг — типизация пользовательского ввода и форм в React: события, значения полей, валидация, а также аккуратная работа с данными из API. На базе сегодняшних хуков это позволит строить стабильные и типобезопасные интерактивные интерфейсы.

    5. Формы и события: типизация обработчиков и валидация

    Формы и события: типизация обработчиков и валидация

    Связь с предыдущими материалами

    В прошлых статьях мы настроили React + TypeScript проект, разобрали базовые типы и научились типизировать компоненты и хуки. Теперь соберём всё в практику для самого частого UI-сценария: формы.

    Цель этой статьи:

  • Научиться типизировать обработчики событий в React без any
  • Понять разницу между target и currentTarget и почему это важно
  • Типобезопасно работать с разными полями: input, textarea, select, checkbox
  • Построить простую, но строгую валидацию на TypeScript
  • Полезные источники:

  • React: Обработка событий
  • React: useState
  • React: Формы
  • TypeScript Handbook: Narrowing
  • !Поток данных: ввод → событие → state → валидация → отправка

    Что такое события в React и какие типы используются

    В React вы работаете с SyntheticEvent (обёртка над нативными событиями браузера). На практике это означает:

  • вы типизируете события типами из React, например React.ChangeEvent
  • у события есть поля target и currentTarget, и они не равнозначны
  • Почему важен currentTarget

  • e.target указывает на фактический элемент, на котором произошло событие (может быть вложенным)
  • e.currentTarget указывает на элемент, на котором висит обработчик
  • Для форм это почти всегда удобнее и безопаснее:

  • в onChange для поля ввода используйте e.currentTarget.value
  • в onSubmit используйте e.currentTarget как саму форму
  • Типизация основных обработчиков

    onChange для текстового input

    Ключевые моменты:

  • React.ChangeEventHandler<HTMLInputElement> даёт строгий тип currentTarget
  • value гарантированно строка
  • onChange для textarea

    onChange для checkbox

    У checkbox значение берётся не из value, а из checked.

    onChange для select

    Здесь мы сделали простое сужение (narrowing) и не позволяем записать в state произвольную строку.

    onSubmit: типизация отправки формы

    React.FormEventHandler<HTMLFormElement> гарантирует, что e.currentTarget является формой.

    Таблица: самые частые события и их типы

    | Сценарий | Prop | Тип обработчика | Где брать данные | |---|---|---|---| | Ввод текста | onChange | React.ChangeEventHandler<HTMLInputElement> | e.currentTarget.value | | Ввод текста в textarea | onChange | React.ChangeEventHandler<HTMLTextAreaElement> | e.currentTarget.value | | Выбор option | onChange | React.ChangeEventHandler<HTMLSelectElement> | e.currentTarget.value | | Чекбокс | onChange | React.ChangeEventHandler<HTMLInputElement> | e.currentTarget.checked | | Отправка формы | onSubmit | React.FormEventHandler<HTMLFormElement> | e.currentTarget | | Клик | onClick | React.MouseEventHandler<HTMLButtonElement> | e.currentTarget | | Потеря фокуса | onBlur | React.FocusEventHandler<HTMLInputElement> | e.currentTarget.value |

    Контролируемые и неконтролируемые формы

    Контролируемая форма

    Контролируемая форма хранит значения полей в React state. Это проще для строгой типизации и валидации.

    Плюсы:

  • простая синхронизация UI и данных
  • удобно валидировать на каждый ввод
  • легко показывать ошибки
  • Минусы:

  • больше кода
  • при очень больших формах надо следить за производительностью
  • Неконтролируемая форма

    Неконтролируемая форма хранит значения внутри DOM, а вы читаете их на submit (обычно через FormData). Это может быть проще, но требует аккуратного сужения типов.

    Почему нужен getStringField:

  • FormData.get возвращает FormDataEntryValue | null
  • FormDataEntryValue это string | File
  • без проверки вы рискуете работать со значением неправильного типа
  • Типобезопасное хранение состояния формы

    Реалистичная форма обычно хранит несколько полей сразу. Удобно завести тип FormValues.

    Обработчик, который обновляет конкретное поле

    Для небольших форм можно писать отдельный onChange на каждое поле. Для более общих случаев часто делают универсальную функцию.

    Вариант для текстовых полей:

    Почему этот подход типобезопасен:

  • key может быть только ключом LoginValues
  • value автоматически должен соответствовать типу поля
  • невозможно случайно записать boolean в email или строку в remember
  • Валидация: типы для ошибок и стратегия отображения

    Минимальная модель ошибок

    Обычно ошибки удобно хранить как объект, где ключи соответствуют ключам формы, а значения это текст ошибки.

    Что здесь происходит:

  • keyof T даёт объединение ключей объекта, например "email" | "password" | "remember"
  • Record<keyof T, string> означает объект, где каждому ключу соответствует строка
  • Partial<...> делает эти поля опциональными, потому что ошибки есть не всегда
  • Функция validate

    Важно: мы валидируем values, а не DOM. Это делает логику независимой от разметки.

    touched: показывать ошибки не сразу

    Пользовательский опыт часто лучше, если ошибки показываются только после того, как поле было в фокусе и потеряло его.

    Обновлять touched удобно на onBlur.

    Полный пример: типобезопасная форма логина с валидацией

    Замечания по типизации:

  • errors и touched связаны с LoginValues через keyof, поэтому структура остаётся синхронизированной
  • setField не позволит присвоить значение неправильного типа
  • обработчики событий берут данные через currentTarget
  • Типичные ошибки и как их избежать

  • Использовать any для событий
  • Читать e.target.value вместо e.currentTarget.value без необходимости
  • Для checkbox читать value вместо checked
  • При чтении из FormData не учитывать File | string | null
  • Хранить ошибки как string[] без привязки к полям, а затем путаться, где какая ошибка
  • Что дальше

    После форм логичный следующий шаг в большом приложении:

  • типобезопасная работа с асинхронными запросами (отправка формы, загрузка справочников)
  • типизация ответов API и обработка ошибок
  • вынос логики форм в переиспользуемые хуки и компоненты
  • Формы и события это место, где TypeScript даёт максимальный эффект: меньше багов из-за несоответствия типов и больше уверенности при рефакторинге UI.

    6. Работа с API: типизация запросов, DTO и управление состоянием

    Работа с API: типизация запросов, DTO и управление состоянием

    Связь с предыдущими материалами

    Ранее мы научились типизировать компоненты, props, state, хуки и формы с типобезопасной валидацией. Следующий логичный шаг в практической разработке фронтенда: получать и отправлять данные на сервер так, чтобы типы не расходились с реальностью, а состояние загрузки было предсказуемым.

    Эта статья отвечает на вопросы:

  • Как типизировать сетевые запросы в TypeScript и не скатиться в any
  • Что такое DTO и зачем отделять его от UI-моделей
  • Как организовать типобезопасное состояние загрузки, ошибки и результат
  • Как избежать гонок запросов и обновлений после размонтирования
  • Полезные источники:

  • React: useEffect
  • MDN: Fetch API
  • TypeScript Handbook
  • !Схема показывает, что данные с сервера приходят как DTO, затем приводятся к модели UI и управляются через состояния загрузки

    Главная идея: типы помогают, но не проверяют реальность

    TypeScript проверяет типы во время разработки, но данные от сервера приходят во время выполнения. Поэтому важно различать:

  • типизацию результата запроса, чтобы код не ломался на уровне логики React
  • проверку данных (runtime), чтобы приложение не падало на неожиданных форматах ответа
  • Практическое правило: начинайте данные от API как unknown, а затем либо проверяйте их, либо явно приводите к типу в одном месте и осознавайте риск.

    DTO: контракт с сервером и почему его нельзя смешивать с UI

    DTO (Data Transfer Object) это структура данных в том виде, в котором она приходит от сервера или уходит на сервер.

    Почему DTO лучше отделять от моделей UI:

  • сервер может присылать поля в другом формате (например, created_at вместо createdAt)
  • сервер может присылать лишние поля, которые UI не должен использовать
  • UI часто удобнее работать с нормализованными данными (например, Date вместо строки)
  • Пример: DTO и модель домена

    Ключевая выгода: если сервер поменял DTO, вы чините в одном месте, а не по всему приложению.

    Типобезопасный результат API: успешный ответ или ошибка

    Во фронтенде почти всегда нужно различать:

  • успех
  • ожидаемую ошибку (например, 400, 401)
  • неожиданную ошибку (например, сеть, 500)
  • Удобно ввести тип результата как дискриминирующее объединение.

    С таким типом код в React будет вынужден обработать обе ветки.

    Обёртка над fetch: один вход для всех запросов

    Базовая функция request

    fetch сам по себе не знает, какой тип данных вернёт response.json(). Поэтому типизация должна быть в вашей обёртке.

    Преимущества:

  • вы защищаетесь от неожиданных данных
  • TypeScript начинает доверять типу только после проверки
  • Типизация запросов: query, body, headers

    Query параметры

    Самый безопасный подход: функция принимает уже типизированные параметры, а строку запроса собирает сама.

    Практические ошибки при работе с API и как их предотвращать

  • Использовать as SomeType в каждом компоненте вместо одного слоя API.
  • Хранить DTO в состоянии и напрямую использовать в JSX.
  • Не учитывать отмену запросов, из-за чего появляются гонки и предупреждения.
  • Смешивать в одном состоянии и данные, и ошибки, и флаги без понятной модели.
  • Обрабатывать ошибки только через console.log, не имея типового сценария error.
  • Рекомендованная структура кода в проекте

    Один из рабочих вариантов для небольшого приложения:

  • src/api/request.ts содержит requestJson, обработку ошибок, общие заголовки.
  • src/api/users.ts содержит функции getUser, listUsers, createUser.
  • src/features/users/types.ts содержит UserDto, User и мапперы.
  • Компоненты используют только функции из src/api/* и модели UI.
  • Так вы избегаете ситуации, когда типы размазаны по всей кодовой базе.

    Что дальше

    После того как вы научились типизировать API и состояние загрузки, следующий практический уровень:

  • унификация кеширования и повторного запроса данных
  • вынос логики запросов в переиспользуемые хуки
  • интеграция отправки форм с API и отображение ошибок сервера рядом с полями
  • На этом фундаменте уже удобно подключать специализированные инструменты для запросов и кеша, но даже без них вы можете построить предсказуемое и типобезопасное взаимодействие с сервером.

    7. Архитектура и качество: generics, утилитные типы, тестирование

    Архитектура и качество: generics, утилитные типы, тестирование

    Связь с предыдущими материалами

    Ранее мы настроили проект, научились типизировать компоненты и хуки, работать с формами и безопасно взаимодействовать с API через DTO, маппинг и модели состояния загрузки.

    Следующий шаг в практической разработке фронтенда: сделать так, чтобы кодовая база оставалась поддерживаемой при росте функциональности. Для этого нужны:

  • архитектурные границы (где живут типы, API, фичи и компоненты)
  • переиспользуемые типовые абстракции через generics и утилитные типы
  • автоматическая проверка качества тестами
  • Полезные ссылки:

  • TypeScript Handbook
  • TypeScript Utility Types
  • Vitest
  • React Testing Library
  • Testing Library: guiding principles
  • Архитектура в TypeScript + React: где должны жить типы и логика

    Хорошая архитектура во фронтенде обычно отвечает на два вопроса:

  • где находится единый источник правды для контрактов (типов) и правил преобразования данных
  • как сделать так, чтобы изменения локализовались и не заставляли править десятки файлов
  • Практическое правило слоёв

    Во всех предыдущих статьях мы постепенно пришли к схеме:

  • API возвращает DTO
  • приложение преобразует DTO в модель UI/домена
  • компоненты рендерят уже UI-модель, а не DTO
  • Это важно по качеству:

  • DTO может меняться независимо от UI
  • UI получает удобные структуры (например, Date, нормальные названия полей)
  • TypeScript-ошибки появляются ближе к месту изменения (маппер), а не размазываются по всему приложению
  • Рекомендуемая структура проекта

    Один из удобных вариантов для небольшого и среднего приложения:

  • src/api/ — слой запросов и ошибок
  • src/features/<feature>/ — фичи, содержащие типы, мапперы, хуки и компоненты
  • src/shared/ — переиспользуемые UI-компоненты и утилиты
  • Пример:

    !Визуальная карта слоёв и направлений зависимостей

    Ключевое правило зависимостей:

  • shared не зависит от features
  • features может зависеть от shared и api
  • компоненты не должны содержать логики преобразования DTO
  • Generics как инструмент архитектуры

    Generics (дженерики) позволяют написать тип и функцию так, чтобы они работали с разными данными, но сохраняли строгую типизацию. Во фронтенде это особенно важно для:

  • API-обёрток
  • моделей удалённых данных (loading, success, error)
  • переиспользуемых хуков
  • утилит для форм
  • Универсальный тип состояния загрузки

    В статье про API мы использовали объединение статусов. Вынесем его в shared/lib/remoteData.ts и сделаем обобщённым:

    Теперь в любой фиче можно писать:

    Плюс качества: компонент физически не сможет обратиться к data, пока status не станет success.

    Обобщённая обёртка над запросом JSON

    Если у вас есть единая функция запросов, её можно сделать обобщённой по типу полезной нагрузки.

    Важно для качества:

  • unknown подчёркивает, что данные пришли в runtime
  • приведение as T лучше концентрировать в одном месте (слой API)
  • если проект растёт, добавляйте runtime-проверку (например, схемами)
  • Для схем и runtime-валидации часто используют:

  • Zod
  • Generic-маппер DTO в модель

    Чтобы не дублировать шаблон преобразования, можно стандартизировать контракт маппера.

    Использование:

    Плюс качества: любой маппер читается одинаково, проще тестировать и искать по проекту.

    Обобщённый setField для форм

    Из статьи про формы у нас был типобезопасный setField. Это типичный пример полезного generics в UI.

    Что делает K extends keyof T:

  • key может быть только ключом объекта T
  • тип value автоматически зависит от конкретного ключа
  • Утилитные типы как способ держать контракты в порядке

    Утилитные типы TypeScript помогают описывать реальные сценарии фронтенда без ручного дублирования типов.

    Самые практичные утилиты и где они применяются

    | Утилитный тип | Что делает | Типичный фронтенд-кейс | |---|---|---| | Partial<T> | делает поля опциональными | состояние формы редактирования | | Required<T> | делает поля обязательными | нормализованные данные после валидации | | Pick<T, K> | выбирает часть полей | карточка/превью в списке | | Omit<T, K> | исключает поля | payload создания без id | | Record<K, V> | словарь | тексты для статусов/ролей | | ReturnType<F> | тип результата функции | вывод типов из фабрик/селекторов | | Parameters<F> | тип параметров функции | проксирование и обёртки | | Awaited<T> | извлекает тип из Promise | вывод типа данных из async-функций | | NonNullable<T> | убирает null и undefined | после явной проверки | | Extract<A, B> | оставляет пересечение union | выделение конкретной ветки состояния | | Exclude<A, B> | убирает из union | запрет части значений |

    Документация:

  • TypeScript Utility Types
  • Пример: тип ошибок формы, связанный со значениями

    Плюс качества: если вы переименовали поле в LoginValues, TypeScript сразу покажет места, где нужно обновить ошибки.

    Пример: DTO и UI-модель и правильные payload-типы

    Плюс качества: вы не копируете руками типы для похожих сценариев.

    Оператор satisfies для более строгих проверок

    satisfies проверяет, что объект соответствует контракту, но при этом не расширяет тип объекта до контракта целиком. Это удобно для конфигов и словарей.

    Плюс качества:

  • если вы забыли ключ, TypeScript покажет ошибку
  • если вы добавили лишний ключ, TypeScript тоже покажет ошибку
  • Подробнее:

  • TypeScript 4.9 satisfies operator
  • Техника качества: исчерпывающая проверка веток

    Когда вы используете дискриминирующие объединения (например, RemoteData<T>), важно гарантировать, что вы обработали все случаи. Это полезно для reducers, маппинга статусов и отображения.

    Утилита assertNever обычно живёт в shared/lib/assertNever.ts:

    Пример применения:

    Плюс качества: если вы добавите новый статус, TypeScript заставит обновить switch.

    Тестирование: как проверять качество в TypeScript + React

    Тесты во фронтенде обычно делят на:

  • юнит-тесты для чистых функций (мапперы, валидаторы)
  • компонентные тесты для UI-логики (рендер, ввод, клики)
  • интеграционные тесты для связок (форма + запрос + отображение ошибки)
  • Главная идея тестирования в React:

  • тестируйте поведение, которое видит пользователь
  • меньше проверяйте внутренние детали реализации
  • Это совпадает с принципами Testing Library:

  • Testing Library: guiding principles
  • Инструменты

    Практичный набор для Vite-проектов:

  • Vitest как тест-раннер
  • React Testing Library для рендера и взаимодействия
  • jest-dom для удобных матчеров
  • Пример: тестируем чистую функцию (маппер)

    Почему это важно архитектурно:

  • мапперы изолируют изменения DTO
  • тест на маппер быстро покажет, что контракт поменялся
  • Пример: тестируем компонент формы как пользователь

    Ниже упрощённый пример, где важна типобезопасная логика валидации, а тест проверяет поведение: кнопка выключена, ошибка появляется после blur.

    Ключевые практики качества:

  • использовать getByRole и getByLabelText, а не селекторы по классу
  • взаимодействовать через userEvent, а не вручную вызывать обработчики
  • Тестирование запросов без реального сервера

    Есть два популярных подхода:

  • мокать fetch напрямую в тестах
  • использовать слой перехвата запросов на уровне сети, чтобы тесты были ближе к реальности
  • Для второго часто используют:

  • Mock Service Worker
  • Плюс качества: вы тестируете сценарий целиком (загрузка, ошибка, повтор), не привязываясь к реализации запроса.

    Мини-чеклист качества для реального проекта

  • npm run typecheck проходит без ошибок
  • типы DTO не протекают в JSX, есть мапперы в одном месте
  • есть общий RemoteData<T> или аналог для статусов загрузки
  • утилитные типы (Pick, Omit, Partial, Record) используются вместо ручного копирования
  • reducers и рендеринг статусов используют исчерпывающую проверку
  • есть тесты на мапперы и валидаторы
  • есть хотя бы несколько тестов поведения для ключевых компонентов
  • Что дальше

    На базе архитектурных границ, generics, утилитных типов и тестов становится проще масштабировать приложение:

  • выделять фичи и переиспользуемые части
  • безопасно менять контракты API
  • переносить UI-логику в хуки и сервисы без потери уверенности
  • Следующий практический уровень обычно связан с унификацией работы с данными: кеширование, повторные запросы, общие хуки загрузки и стратегий ошибок, но даже без дополнительных библиотек описанные принципы дают сильный фундамент качества.