← назад к разделу

JavaScript ловит ошибки в момент запуска — когда программа уже работает у пользователя. TypeScript ловит ошибки раньше: во время написания кода в редакторе. Это меняет то, как чувствуется разработка: вместо «запустил и проверил» получаешь мгновенную подсказку прямо в коде.

В React TypeScript особенно полезен в нескольких местах: пропсы компонентов, события, состояния и ссылки на DOM-элементы. Разберём каждое.

Зачем TypeScript, если уже есть JavaScript

В чистом JavaScript компонент принимает что угодно. Допустим, есть кнопка:

function Button({ variant, onClick, children }) {
  return <button className={variant} onClick={onClick}>{children}</button>;
}

Ничто не мешает вызвать её так: <Button variant="primry" /> — опечатка в названии варианта, onClick не передан. JavaScript промолчит. Пользователь увидит сломанную кнопку.

TypeScript добавляет описание того, что компонент ожидает. Передал не то — редактор подчеркнул ошибку ещё до запуска. Это и есть статическая типизация: проверка до выполнения, а не во время.

Типизация пропсов

Пропсы — это данные, которые родитель передаёт компоненту. Описывают их через type или interface. На практике чаще используют type — он чуть гибче.

type ButtonProps = {
  variant: "primary" | "secondary";
  disabled?: boolean;
  onClick: () => void;
  children: React.ReactNode;
};

function Button({ variant, disabled = false, onClick, children }: ButtonProps) {
  return (
    <button className={variant} disabled={disabled} onClick={onClick}>
      {children}
    </button>
  );
}

Несколько деталей:

  • "primary" | "secondary" — вместо просто string. Теперь variant="primry" не пройдёт компиляцию, а редактор предложит варианты.
  • disabled? — вопросительный знак означает, что проп необязательный.
  • React.ReactNode — тип для children: принимает текст, элементы, массивы и null.

Переиспользуемые компоненты и обобщённые типы

Представьте список товаров. Потом список заказов. Потом список пользователей. Каждый раз писать отдельный компонент списка неудобно — проще один, который работает с любыми данными.

Для этого TypeScript предлагает обобщённые типы (generics). Буква T в угловых скобках — это «любой тип, который передаст вызывающая сторона»:

type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyOf: (item: T) => string;
};

function List<T>({ items, renderItem, keyOf }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyOf(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

Когда используем List<Product>, TypeScript знает: renderItem получает Product, не что-нибудь. Если попробовать обратиться к несуществующему полю — ошибка в редакторе, а не в браузере.

Состояния-с-несколькими-вариантами

Вот распространённая ситуация: компонент грузит данные с сервера. Пока грузит — спиннер, если ошибка — сообщение, если готово — данные. Как это описать?

Первый порыв — набор необязательных флагов:

type State = {
  isLoading?: boolean;
  error?: string;
  data?: Product;
};

Проблема: такой тип допускает бессмысленные комбинации — например, isLoading: true и data: {...} одновременно. TypeScript не возразит, а компонент поведёт себя непредсказуемо.

Решение — дискриминированный union: объединение типов с общим полем-меткой (status), по которому TypeScript понимает, в каком именно состоянии мы находимся.

type RemoteData<T> =
  | { status: "loading" }
  | { status: "error"; error: string }
  | { status: "success"; data: T };

function ProductView({ state }: { state: RemoteData<Product> }) {
  switch (state.status) {
    case "loading": return <Spinner />;
    case "error":   return <ErrorBox message={state.error} />;
    case "success": return <ProductCard product={state.data} />;
  }
}

В каждой ветке switch TypeScript точно знает, какие поля доступны: в ветке "success" есть state.data, в "error"state.error. Попытка обратиться к полю другой ветки — ошибка компиляции.

Типизация событий и ref

React-события имеют конкретные типы. Тип зависит от того, на каком элементе происходит событие и какой это обработчик.

function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    inputRef.current?.focus();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} onChange={handleChange} />
    </form>
  );
}

Полезные типы событий:

  • React.ChangeEvent<HTMLInputElement> — изменение поля ввода
  • React.FormEvent<HTMLFormElement> — отправка формы
  • React.MouseEvent<HTMLButtonElement> — клик по кнопке

useRef<HTMLInputElement>(null) — типизированная ссылка на DOM-элемент. Редактор подскажет все доступные методы и свойства элемента.

Производные типы: не повторяй себя

TypeScript позволяет строить новые типы из уже существующих, не копируя их. Три часто встречающихся приёма:

type Product = {
  id: string;
  name: string;
  price: number;
  createdAt: string;
};

// Форма создания: всё, кроме серверных полей
type CreateProductInput = Omit<Product, "id" | "createdAt">;

// Форма редактирования: все поля необязательны
type UpdateProductInput = Partial<Product>;

// Только название и цена для карточки-превью
type ProductPreview = Pick<Product, "name" | "price">;

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

Коротко

  • TypeScript проверяет типы до запуска — ошибки видны в редакторе, а не у пользователя.
  • Пропсы описывают через type или interface; узкие union строковых литералов вместо string ловят опечатки.
  • Переиспользуемые компоненты (список, таблица, селект) делают обобщёнными через <T>, чтобы не терять типы.
  • Взаимоисключающие состояния описывают дискриминированным union с полем-меткой — это убирает невозможные комбинации.
  • Тип события зависит от элемента: React.ChangeEvent<HTMLInputElement>, React.MouseEvent<HTMLButtonElement> и т.д.
  • useRef<HTMLElement>(null) — типизированная ссылка на DOM.
  • Omit, Partial, Pick — строят новые типы из существующих без дублирования.

Что почитать дальше

  • Хуки: кастомные и ловушки useEffect — как типизировать кастомные хуки.
  • Формы и валидация — типы для полей формы и схемы валидации.
  • Структура проекта и компоненты — как организовать типизированные компоненты в проекте.