TypeScript на frontend — это та же граница доверия, что типы в backend: внутри приложения данные имеют форму, и компилятор её держит. Но типизировать можно по-разному. any и as превращают TypeScript в декорацию; точные типы превращают его в инструмент, который ловит ошибки до рантайма. В React несколько мест, где это особенно окупается: пропсы, состояния, события.

Пропсы

Пропсы компонента типизируют явно. type или interface — оба работают; на практике type чуть гибче (union, пересечения), и команды чаще берут его по умолчанию.

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>;
}

Главный приём — union строковых литералов ("primary" | "secondary") вместо string: компилятор не даст передать variant="primry", а редактор подскажет варианты. Чем уже тип, тем больше ошибок ловится статически. children типизируют как React.ReactNode.

Generics для переиспользуемых компонентов

Когда компонент работает с данными любого типа — список, таблица, селект, — его делают обобщённым (generic), чтобы он сохранял типы, а не сваливал всё в any.

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> знает, что renderItem получает Product, — типы текут сквозь компонент. Это убирает соблазн типизировать переиспользуемые компоненты как any[].

Дискриминированные union для состояний

Частая ошибка — набор необязательных пропсов, описывающих взаимоисключающие состояния (isLoading?, error?, data?), допускающий бессмысленные комбинации (и грузится, и ошибка). Дискриминированный union делает невозможные состояния невыразимыми.

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 {...state.data} />;
  }
}

В каждой ветке switch TypeScript знает, какие поля доступны (state.data есть только в success). Это «делать недопустимые состояния непредставимыми» — тот же приём, что типобезопасность домена в backend.

События и ref

React-события и ref типизированы. Тип события зависит от элемента и обработчика; ref — от элемента, на который ссылается.

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

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

  return <input ref={inputRef} onChange={onChange} />;
}

React.ChangeEvent<HTMLInputElement>, React.FormEvent, React.MouseEvent — типы событий; useRef<HTMLInputElement>(null) типизирует ref. Редактор подскажет доступные свойства, а опечатка в e.taget не пройдёт компиляцию.

Утилитарные типы

Производные типы не дублируют, а выводят из существующих: Pick, Omit, Partial. Например, форма создания — это сущность без серверных полей:

type Product = { id: string; name: string; price: number; createdAt: string };
type CreateProductInput = Omit<Product, "id" | "createdAt">;

Меняется ProductCreateProductInput следует за ним. Это «единый источник истины» на уровне типов: один тип-родитель, остальные выведены.

Где это в UCP

Точная типизация — граница доверия внутри frontend, как типы между слоями backend: пропсы узкими union'ами, переиспользуемое — generics, состояния — дискриминированными union, производное — утилитарными типами. В связке с zod-схемами форм и типами из контракта API это даёт сквозную типобезопасность от сервера до экрана — то, что позволяет продукт-инженеру менять продукт, ловя рассинхрон компилятором, а не в проде. Дальше типы встречаются с логикой — в хуках.