Рекомендации по разработке на TypeScript
Дальше: Форматы и подключение · Обработка ошибок · Справочник — лучшие практики
TypeScript даёт максимум пользы, когда команда договорилась о правилах — strict-флаги, работа с внешними данными, слои типов, CI и постепенная миграция с JavaScript. Здесь — инженерные практики; синтаксис — в Типы данных и типизация в TypeScript и Функции в TypeScript. Таблицы флагов — в Справочник — лучшие практики.
Базовые правила
- Включайте
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, unknown и явные типы
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.
Структура типов в репозитории
Организация файлов в TypeScript-проекте отделяет контракты данных от логики. Типы держат рядом с доменом, в отдельном каталоге, а не в одном общем "мешке".
src/
types/
api.ts # DTO запросов/ответов
domain.ts # сущности предметной области
mappers/
user.ts # api → domain
services/
controllers/
| Слой | Содержимое | Пример имён файлов |
|---|---|---|
types/api.ts | То, что приходит по HTTP | UserDto, CreateOrderRequest |
types/domain.ts | Правила бизнеса | User, Order |
mappers/ | Явное преобразование DTO → domain | toUser(dto): User |
components/ (frontend) | props, локальный UI-state | ButtonProps, не DTO |
*.test.ts | рядом с модулем или в __tests__/ | — |
Принципы:
- Один модуль — одна ответственность: не смешивать DTO API и props React-компонента.
- Именование: суффиксы
Dto,Request,Responseдля сети; без суффикса — domain. - Barrel-файлы (
types/index.tsс реэкспортом) — по желанию команды; на больших проектах иногда замедляют сборку. - Коллокация: тип, используемый только в одном модуле, можно держать в том же файле; общий контракт — в
types/.
Структура каталогов в monorepo и CI — Экосистема §структура пакета.
Не смешивайте 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-курс: поэтапная миграция.
Апгрейд на TypeScript 7.0
TypeScript 7 — смена движка компилятора (Go), а не языка: tsc --noEmit на чистом 6.0 обычно проходит и на 7.0. Перед обновлением:
- Перейдите на TypeScript 6.0 и уберите
ignoreDeprecations— снимите предупреждения оmoduleResolution: node10,baseUrl,target: es5и т.д. - Проверьте зависимости: typescript-eslint, кастомные трансформеры, Volar — нужен ли
@typescript/typescript6side-by-side (подробнее). - На CI с малым RAM зафиксируйте
--checkers/--builders, чтобы сборки не расходились по памяти. - Полный
build+ тесты — не ограничивайтесь толькоtsc --noEmit, если есть emit metadata или decorator-фреймворки.
npm install -D typescript@^7
npm run typecheck
Антипаттерны
| Антипаттерн | Почему плохо |
|---|---|
| Откладывать типы на потом | Долг растёт быстрее, чем сокращается |
@ts-ignore без комментария | Скрывает реальные баги |
as User на response.json() | Ложная уверенность |
Один огромный types.ts | Сложно ревьюить и искать |
| Отключить strict в CI | Регрессии проходят незамеченными |
Допустимый as — после проверки или в тестовых заглушках с пояснением.
@ts-expect-error и @ts-ignore
Директивы в комментарии отключают проверку TypeScript на следующей строке. Используйте редко и только с объяснением.
| Директива | Поведение | Когда применять |
|---|---|---|
@ts-ignore | подавляет любую ошибку на следующей строке | почти никогда в новом коде |
@ts-expect-error | подавляет ошибку; падает сборка, если ошибки уже нет | ожидаемый баг TS, тесты на типы, временный workaround |
// @ts-expect-error — демонстрация: number нельзя присвоить string
const broken: string = 42;
// @ts-ignore — ошибка исчезнет и если строка станет валидной (опасно)
// @ts-ignore
const alsoBroken: string = 42;
Правила команды:
- Предпочитайте исправление типов,
unknown+ guard, корректные@types— не комментарии. - Если
@ts-expect-errorнеизбежен — одна строка, пояснение почему и ссылка на issue / версию TS. - Запретите
@ts-ignoreв ESLint (@typescript-eslint/ban-ts-comment) или требуйтеts-expect-errorс описанием. - После обновления TypeScript пересматривайте директивы:
@ts-expect-errorбез ошибки сломаетtsc.
Синтаксис утверждений 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с функцией-сужением.