Перейти к основному содержимому

TypeScript и React

Разработчику Архитектору

Дальше: TypeScript и Node.js · События · Асинхронность · Справочник — интеграция с фреймворками


React описывает UI как функции от состояния. TypeScript добавляет контракты: какие props обязательны, что лежит в state, какие поля пришли с API. Это снижает поломки при рефакторинге компонентов и согласует фронт с backend-DTO.

Маршрут: Типы и типизацияАсинхронностьReactобработка ошибок.

Курс по React: справочник 271. Старт проекта — 4.md + npm create vite@latest -- --template react-ts.


Интеграция с фреймворками (обзор)

Эта статья — React; общая картина Справочник — интеграция с фреймворками:

СтекШаблон / типыУчебный материал
Reactreact-ts, @types/reactэта статья, 271
Vuevue-tsc, defineComponent281
Angularвстроенный TS, strict templates291
Node.js@types/node, DTO API22.md

Общие принципы для всех: типизировать props/inputs, ответы API, состояния загрузки (discriminated union) — 10.md, 17.md.


TypeScript в React-проектах

Без TSС TS
Опечатка в props — в runtimeОшибка при сборке / в IDE
Неверное поле от APIПодсветка при доступе
Рефакторинг prop nameFind references + компилятор
useState(null) — любой типuseState<User | null>(null)

TypeScript не заменяет тесты и не валидирует JSON с сервера сам — для API нужны guards или Zod — 6.md, 17.md.


Старт: Vite + React + TS

npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev

Разбор:

  • Шаблон даёт tsconfig.app.json, типы React, ESLint.
  • Сборку делает Vite; типы проверяют tsc -bpackage.json часто в скрипте build перед vite build). Отдельно для CI удобно tsc --noEmit.

Структура типов:

src/
├── components/
├── types/
│ └── api.ts # DTO и LoadState
└── App.tsx

Компонент и props

type CardProps = {
title: string;
subtitle?: string;
children?: React.ReactNode;
};

function Card({ title, subtitle, children }: CardProps) {
return (
<section className="card">
<h2>{title}</h2>
{subtitle ? <p>{subtitle}</p> : null}
{children}
</section>
);
}

Разбор:

  • children?: React.ReactNode — текст, элемент, массив, null.
  • Не обязательно React.FC — явный тип props у функции читаемее; в 301 описаны варианты.

Расширение props нативного элемента

type ButtonProps = {
label: string;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

function Button({ label, ...rest }: ButtonProps) {
return <button type="button" {...rest}>{label}</button>;
}

Разбор:

  • ...rest пробрасывает onClick, disabled, className с проверкой типов.

useState с явным типом

type User = { id: string; name: string };

function UserBadge() {
const [user, setUser] = React.useState<User | null>(null);

if (!user) return <p>Не авторизован</p>;
return <span>{user.name}</span>;
}

Разбор:

  • useState(null) без generic в strict даёт только null — укажите User | null.
  • Обновление: setUser({ id: "1", name: "Ann" }) проверяется на соответствие User.

LoadState в компоненте

Тот же discriminated union, что в 17.md:

type User = { id: string; name: string };

type LoadState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; user: User }
| { status: "error"; message: string };

function Profile() {
const [state, setState] = React.useState<LoadState>({ status: "idle" });

React.useEffect(() => {
let cancelled = false;

async function load() {
setState({ status: "loading" });
try {
const res = await fetch("/api/me");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const raw: unknown = await res.json();
if (!isUser(raw)) throw new Error("Invalid payload");
if (!cancelled) setState({ status: "success", user: raw });
} catch (e: unknown) {
if (!cancelled) {
setState({
status: "error",
message: e instanceof Error ? e.message : "Unknown",
});
}
}
}

load();
return () => {
cancelled = true;
};
}, []);

switch (state.status) {
case "idle":
return <p>Нажмите для загрузки</p>;
case "loading":
return <p>Загрузка…</p>;
case "error":
return <p>Ошибка: {state.message}</p>;
case "success":
return <h2>{state.user.name}</h2>;
}
}

function isUser(v: unknown): v is User {
if (typeof v !== "object" || v === null) return false;
const o = v as Record<string, unknown>;
return typeof o.id === "string" && typeof o.name === "string";
}

Разбор:

  • cancelled в cleanup — защита от setState после размонтирования.
  • switch по status сужает тип без лишних флагов isLoading.

useReducer с типизированными actions

Когда состояние и переходы сложнее, чем один setState, удобен discriminated union для действий:

type CartItem = { sku: string; qty: number };

type CartState = { items: CartItem[] };

type CartAction =
| { type: "add"; sku: string }
| { type: "remove"; sku: string }
| { type: "clear" };

function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case "add":
return {
items: [...state.items, { sku: action.sku, qty: 1 }],
};
case "remove":
return {
items: state.items.filter((i) => i.sku !== action.sku),
};
case "clear":
return { items: [] };
default: {
const _exhaustive: never = action;
return _exhaustive;
}
}
}

function Cart() {
const [state, dispatch] = React.useReducer(cartReducer, { items: [] });

return (
<button type="button" onClick={() => dispatch({ type: "add", sku: "book-1" })}>
Добавить ({state.items.length})
</button>
);
}

Разбор:

  • CartAction — union с полем type; в ветке switch доступны только нужные поля (sku в add).
  • dispatch({ type: "addd", sku: "x" }) — ошибка компиляции (опечатка в type).
  • Тот же приём, что для LoadState, но для событий UI28.md.

ComponentProps — переиспользование props

Не дублируйте поля нативного элемента — возьмите тип у готового компонента или HTML-тега:

type IconButtonProps = {
label: string;
} & React.ComponentProps<"button">;

function IconButton({ label, ...rest }: IconButtonProps) {
return (
<button type="button" aria-label={label} {...rest}>

</button>
);
}

type NativeInputProps = React.ComponentProps<"input">;
type SearchInputProps = Pick<NativeInputProps, "placeholder" | "disabled">;

Разбор:

  • ComponentProps<"button"> — все допустимые props <button> с типами событий.
  • Pick оставляет в публичном API только нужные поля обёртки.

События форм

function SearchBox({ onSearch }: { onSearch: (q: string) => void }) {
const [q, setQ] = React.useState("");

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

const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSearch(q.trim());
};

return (
<form onSubmit={onSubmit}>
<input value={q} onChange={onChange} aria-label="Поиск" />
<button type="submit">Найти</button>
</form>
);
}
Тип событияЭлемент
ChangeEvent<HTMLInputElement><input>
ChangeEvent<HTMLSelectElement><select>
FormEvent<HTMLFormElement><form onSubmit>
MouseEvent<HTMLButtonElement><button onClick>

Подробнее DOM — 20.md.


useRef и DOM

function FocusInput() {
const ref = React.useRef<HTMLInputElement>(null);

const focus = () => ref.current?.focus();

return (
<>
<input ref={ref} />
<button type="button" onClick={focus}>Фокус</button>
</>
);
}

Разбор:

  • ref.current имеет тип HTMLInputElement | null — нужен optional chaining.

Контекст с типом

type Theme = "light" | "dark";

type ThemeContextValue = {
theme: Theme;
setTheme: (t: Theme) => void;
};

const ThemeContext = React.createContext<ThemeContextValue | null>(null);

function useTheme(): ThemeContextValue {
const ctx = React.useContext(ThemeContext);
if (!ctx) throw new Error("ThemeProvider missing");
return ctx;
}

Разбор:

  • null по умолчанию заставляет проверять провайдер в хуке.
  • Альтернатива — значение по умолчанию в createContext (менее строго).

Слой типов API

src/types/api.ts:

export type UserDto = { id: string; name: string; email: string };
export type CreateUserBody = Pick<UserDto, "name" | "email">;

Компонент импортирует только типы:

import type { UserDto } from "../types/api.js";

Разбор:

  • DTO ≠ domain: на границе сети — отдельные типы — 6.md, 22.md.
  • import type не попадает в JS-бандл.

Дженерики в компонентах

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

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

См. 24.md.


Частые ошибки

ОшибкаЧто делать
props: anyявный type Props
as User на json()unknown + guard
Разрозненные loading/errorLoadState union
Забыли children в типеReact.ReactNode
React.FC с implicit childrenявные props (стиль команды)
Типы API в каждом файлеtypes/api.ts

Практика

  1. Создайте react-ts проект и компонент Card с typed props.
  2. Реализуйте Profile с LoadState и isUser guard.
  3. Форма с onSubmit и ChangeEvent<HTMLInputElement>.
  4. Вынесите UserDto в types/api.ts, используйте import type.
  5. Добавьте generic List<T> для массива задач.
  6. Перепишите локальное состояние корзины на useReducer с union actions.

Смежные статьи