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; общая картина Справочник — интеграция с фреймворками:
| Стек | Шаблон / типы | Учебный материал |
|---|---|---|
| React | react-ts, @types/react | эта статья, 271 |
| Vue | vue-tsc, defineComponent | 281 |
| Angular | встроенный TS, strict templates | 291 |
| Node.js | @types/node, DTO API | 22.md |
Общие принципы для всех: типизировать props/inputs, ответы API, состояния загрузки (discriminated union) — 10.md, 17.md.
TypeScript в React-проектах
| Без TS | С TS |
|---|---|
Опечатка в props — в runtime | Ошибка при сборке / в IDE |
| Неверное поле от API | Подсветка при доступе |
| Рефакторинг prop name | Find 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 -b(вpackage.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, но для событий UI — 28.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";
Разбор:
Дженерики в компонентах
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/error | LoadState union |
Забыли children в типе | React.ReactNode |
React.FC с implicit children | явные props (стиль команды) |
| Типы API в каждом файле | types/api.ts |
Практика
- Создайте
react-tsпроект и компонентCardс typed props. - Реализуйте
ProfileсLoadStateиisUserguard. - Форма с
onSubmitиChangeEvent<HTMLInputElement>. - Вынесите
UserDtoвtypes/api.ts, используйтеimport type. - Добавьте generic
List<T>для массива задач. - Перепишите локальное состояние корзины на
useReducerс union actions.
Смежные статьи
- Асинхронность · События · Ошибки
- Простые приложения · Паттерны
- Синтаксис —
.tsx,import type - 271 · Справочник — интеграция с фреймворками