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

Рекомендации по разработке на TypeScript

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

Дальше: Форматы и подключение · Обработка ошибок · Справочник — лучшие практики


TypeScript даёт максимум пользы, когда команда договорилась о правилах — strict-флаги, работа с внешними данными, слои типов, CI и постепенная миграция с JavaScript. Здесь — инженерные практики; синтаксис — в Типы данных и типизация в TypeScript и Функции в TypeScript. Таблицы флагов — в Справочник — лучшие практики.


Базовые правила

  1. Включайте strict: true в новых проектах.
  2. Минимизируйте any; для неизвестных данных — unknown + проверка.
  3. Проектируйте типы до реализации для API и доменных сущностей.
  4. Разделяйте DTO (сеть) и domain (бизнес-логика).
  5. Гоняйте 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Базовый набор строгих проверок
noUncheckedIndexedAccessarr[i]T | undefined
useUnknownInCatchVariablescatch (e: unknown)
skipLibCheckБыстрее сборка; не проверять .d.ts в node_modules

Флаг noUncheckedIndexedAccess часто даёт первую волну ошибок в legacy-коде: arr[i] становится T | undefined, и это нормально при переходе на strict. useUnknownInCatchVariables убирает неявный any в catch (e) — сочетается с обработкой ошибок.

Полная таблица — Справочник — флаги безопасности.

Legacy-проекты

В старом коде включайте strict по флагам или по папкам. В новом коде — сразу полный strict, иначе типовой долг растёт быстрее, чем сокращается.


any, unknown и явные типы

anyunknownЯвный тип
Проверка компилятораОтключенаНужен 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То, что приходит по HTTPUserDto, CreateOrderRequest
types/domain.tsПравила бизнесаUser, Order
mappers/Явное преобразование DTO → domaintoUser(dto): User
components/ (frontend)props, локальный UI-stateButtonProps, не 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

  1. Опишите union состояний (загрузка / успех / ошибка).
  2. Опишите сигнатуры сервисов и handlers.
  3. Реализуйте код — компилятор подскажет пропущенные ветки.

Пример состояния UI — в типы §discriminated union и Асинхронность.


Миграция с JavaScript

TypeScript рассчитан на постепенный переход:

ШагДействие
1npm i -D typescript, npx tsc --init
2"allowJs": true — TS видит .js
3"checkJs": true + JSDoc @param в .js (опционально)
4Переименование .js.ts по файлу
5npm 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. Перед обновлением:

  1. Перейдите на TypeScript 6.0 и уберите ignoreDeprecations — снимите предупреждения о moduleResolution: node10, baseUrl, target: es5 и т.д.
  2. Проверьте зависимости: typescript-eslint, кастомные трансформеры, Volar — нужен ли @typescript/typescript6 side-by-side (подробнее).
  3. На CI с малым RAM зафиксируйте --checkers / --builders, чтобы сборки не расходились по памяти.
  4. Полный 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;

Правила команды:

  1. Предпочитайте исправление типов, unknown + guard, корректные @types — не комментарии.
  2. Если @ts-expect-error неизбежен — одна строка, пояснение почему и ссылка на issue / версию TS.
  3. Запретите @ts-ignore в ESLint (@typescript-eslint/ban-ts-comment) или требуйте ts-expect-error с описанием.
  4. После обновления 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 без маппера.

Практика

  1. Включите strict в учебном проекте и исправьте все ошибки в одном модуле.
  2. Вынесите типы API в types/api.ts и domain в types/domain.ts.
  3. Добавьте npm run typecheck в CI или pre-commit.
  4. Перепишите один .js-файл в .ts с allowJs: true.
  5. Замените один any на unknown с функцией-сужением.

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