Рекомендации по разработке на TypeScript
Дальше: Форматы и подключение · Обработка ошибок · Справочник — лучшие практики
Эта статья раскрывает Справочник — лучшие практики в формате правил команды: strict, слои типов, миграция, CI.
TypeScript даёт максимум пользы, когда команда договорилась о правилах: strict-флаги, работа с внешними данными, слои типов, CI и постепенная миграция с JavaScript. Здесь — инженерные практики; синтаксис — в 10 и 14.
Базовые правила
- Включайте
strict: trueв новых проектах. - Минимизируйте
any; для неизвестных данных —unknown+ проверка. - Проектируйте типы до реализации для API и доменных сущностей.
- Разделяйте DTO (сеть) и domain (бизнес-логика).
- Гоняйте
tsc --noEmitв CI на каждый PR.
Рекомендуемый tsconfig для нового проекта
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"useUnknownInCatchVariables": true,
"skipLibCheck": true
}
}
| Флаг | Назначение |
|---|---|
strict | Базовый набор строгих проверок |
noUncheckedIndexedAccess | arr[i] → T | undefined |
useUnknownInCatchVariables | catch (e: unknown) |
skipLibCheck | Быстрее сборка; не проверять .d.ts в node_modules |
Флаг noUncheckedIndexedAccess часто даёт первую волну ошибок в legacy-коде: arr[i] становится T | undefined, и это нормально при переходе на strict. useUnknownInCatchVariables убирает неявный any в catch (e) — сочетается с обработкой ошибок.
Полная таблица — Справочник — флаги безопасности.
В старом коде включайте strict по флагам или по папкам. В новом коде — сразу полный strict, иначе типовой долг растёт быстрее, чем сокращается.
any vs unknown vs явные типы
any | unknown | Явный тип | |
|---|---|---|---|
| Проверка компилятора | Отключена | Нужен narrowing | Полная |
| Внешний JSON | Плохо | Хорошо | После валидации |
| Временный костыль | Допустимо с TODO | Лучше | — |
function handlePayload(raw: unknown): User {
if (typeof raw !== "object" || raw === null) {
throw new Error("Invalid payload");
}
const o = raw as Record<string, unknown>;
if (typeof o.id !== "string" || typeof o.name !== "string") {
throw new Error("Invalid user");
}
return { id: o.id, name: o.name };
}
Валидация в production
Ручные проверки масштабируются плохо. Часто используют Zod или Valibot: схема → parse → тип:
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
});
type User = z.infer<typeof UserSchema>;
function parseUser(raw: unknown): User {
return UserSchema.parse(raw);
}
Разбор:
z.inferсвязывает runtime-схему и TypeScript-тип.- При ошибке валидации бросайте понятное исключение вместо тихого
undefined.
Структура типов в репозитории
src/
types/
api.ts # DTO запросов/ответов
domain.ts # сущности предметной области
mappers/
user.ts # api → domain
services/
controllers/
| Слой | Содержимое |
|---|---|
types/api.ts | То, что приходит по HTTP |
types/domain.ts | Правила бизнеса |
mappers/ | Явное преобразование DTO → domain, без скрытой логики в UI |
Не смешивайте DTO React-компонента и ответ API в одном interface — при смене API сломается UI без явной ошибки в маппере.
Type-driven development
- Опишите union состояний (загрузка / успех / ошибка).
- Опишите сигнатуры сервисов и handlers.
- Реализуйте код — компилятор подскажет пропущенные ветки.
Пример состояния UI — в типы §discriminated union и Асинхронность.
Миграция с JavaScript
TypeScript рассчитан на постепенный переход:
| Шаг | Действие |
|---|---|
| 1 | npm i -D typescript, npx tsc --init |
| 2 | "allowJs": true — TS видит .js |
| 3 | "checkJs": true + JSDoc @param в .js (опционально) |
| 4 | Переименование .js → .ts по файлу |
| 5 | npm i -D @types/* для библиотек |
| 6 | Включение strict по мере исправлений |
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": false
}
}
После стабилизации — strict: true и отключение allowJs для переписанных папок.
Краткий указатель в JS-курсе — JS-курс: поэтапная миграция.
Антипаттерны
| Антипаттерн | Почему плохо |
|---|---|
| Откладывать типы на потом | Долг растёт быстрее, чем сокращается |
@ts-ignore без комментария | Скрывает реальные баги |
as User на response.json() | Ложная уверенность |
Один огромный types.ts | Сложно ревьюить и искать |
| Отключить strict в CI | Регрессии проходят незамеченными |
Допустимый as — после проверки или в тестовых заглушках с пояснением.
CI и локальный workflow
package.json:
{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc",
"lint": "eslint . --ext .ts,.tsx"
}
}
В CI минимум:
npm ci
npm run typecheck
npm test
Разделите typecheck и bundle: Vite может собрать проект даже при ошибках, если tsc не в цепочке build.
Чек-лист code review
- Нет нового
anyбез комментария с причиной и сроком исправления. - Публичные экспорты типизированы (параметры и возврат).
-
catchиспользуетunknown. - Union-ветки покрыты или есть exhaustive
neverвdefault. - Внешние данные проходят parse/validate, не
as. - Нет дублирования DTO и domain без маппера.
Практика
- Включите
strictв учебном проекте и исправьте все ошибки в одном модуле. - Вынесите типы API в
types/api.tsи domain вtypes/domain.ts. - Добавьте
npm run typecheckв CI или pre-commit. - Перепишите один
.js-файл в.tsсallowJs: true. - Замените один
anyнаunknownс функцией-сужением.