Опирается на правила: NODE-2NODE-5, NODE-X1, NODE-X2 из Node Style Guide → раздел 1. Инструменты.

Важно знать

  • tsconfig.json должен содержать "strict": true и "noUncheckedIndexedAccess": true; module и moduleResolutionNodeNext.
  • ESLint настраивается с typescript-eslint и пресетом strictTypeChecked; конфиг — единственный файл eslint.config.mjs на сервис.
  • Prettier — форматирование, не предмет споров на ревью; запускается в CI как prettier --check ..
  • CI-прогон: tsc --noEmit + eslint . + prettier --check . на каждой ветке; падение при любом нарушении.
  • @ts-ignore запрещён категорически; только @ts-expect-error с кодом ошибки и обоснованием.
  • // eslint-disable без кода правила и обоснования — нарушение; отключение правил «потому что мешают» создаёт расхождение стиля между сервисами.
  • ESLint и Prettier покрывают механику (нейминг, импорты, формат); семантику разделов 4–7 — ревью скиллом ucp-node-style-review.

Качество кодовой базы держит не культура, а инструменты. Культура — волатильна: меняется команда, меняются привычки. Инструмент, встроенный в CI, работает одинаково для всех и не устаёт. Три инструмента ниже — обязательный минимум на каждом TypeScript-сервисе.

tsconfig.json — strict и NodeNext

NODE-2: "strict": true включает группу проверок компилятора, каждая из которых по отдельности поймала бы дефект только в runtime.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "outDir": "dist",
    "rootDir": "src",
    "esModuleInterop": true,
    "skipLibCheck": false
  }
}

"strict": true разворачивается в восемь флагов:

ФлагЧто поймает
strictNullChecksобращение к null/undefined без проверки
strictFunctionTypesнесовместимые сигнатуры коллбэков
strictBindCallApplyневерные аргументы bind/call/apply
strictPropertyInitializationполя класса без инициализации
noImplicitAnyнеаннотированные параметры, которые вывелись в any
noImplicitThisthis вне класса без явного типа
alwaysStrict"use strict" во всех файлах
useUnknownInCatchVariablescatch (e)e будет unknown, не any

noUncheckedIndexedAccess не входит в strict, но обязателен отдельно. Без него array[0] имеет тип T, а не T | undefined, даже если массив пуст. С флагом компилятор требует проверки перед обращением:

const items: Product[] = getProducts();

const first = items[0];
first.name;

const first = items[0];
if (first !== undefined) {
  first.name;
}

module: "NodeNext" + moduleResolution: "NodeNext" — единственный корректный режим для Node.js ESM. При других значениях TypeScript не резолвит .js-расширения в импортах и не понимает package.json exports.

Ослабить любой из этих флагов — только точечно и с обоснованием в PR. // @ts-nocheck на уровне файла запрещён эквивалентно @ts-ignore.

ESLint — strictTypeChecked пресет

NODE-3: ESLint с typescript-eslint, type-checked пресет, единственный конфиг на сервис.

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import simpleImportSort from 'eslint-plugin-simple-import-sort';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.strictTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        project: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    plugins: {
      'simple-import-sort': simpleImportSort,
    },
    rules: {
      'simple-import-sort/imports': 'error',
      'simple-import-sort/exports': 'error',
      '@typescript-eslint/no-floating-promises': 'error',
      '@typescript-eslint/no-misused-promises': 'error',
      '@typescript-eslint/explicit-function-return-type': 'error',
    },
  },
);

strictTypeChecked — superset recommendedTypeChecked. Включает правила, которые требуют type information от TypeScript-языкового сервера: no-floating-promises, no-misused-promises, no-unsafe-assignment, no-unsafe-call, no-unsafe-return. Эти ошибки не поймает tsc --noEmit — они семантические, а не синтаксические.

Конфиг — один файл eslint.config.mjs в корне сервиса. Разрозненные .eslintrc в поддиректориях запрещены: несколько конфигов создают расхождение правил между частями одного сервиса, и ни один инструмент не покажет это явно.

project-root/
  eslint.config.mjs   ✓ — единственный конфиг
  src/
    order/
      .eslintrc        ✗ — переопределение без обоснования

Prettier — форматирование как данность

NODE-3: Prettier настраивается один раз и не обсуждается на ревью.

{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2,
  "endOfLine": "lf"
}

Prettier не оставляет места для личных предпочтений — это его ключевое свойство. Выбор между " и ', между trailing comma и без него — решается один раз в .prettierrc, остальное делает prettier --write перед коммитом.

В CI — только prettier --check ., не --write. Автоматическая правка кода в CI создаёт коммиты от имени бота и усложняет историю. Правка — на стороне разработчика, проверка — в CI.

npx prettier --check "src/**/*.{ts,json}"

CI — три команды на каждом прогоне

NODE-4: обязательная тройка в каждом CI-pipeline.

- name: Type check
  run: npx tsc --noEmit

- name: Lint
  run: npx eslint .

- name: Format check
  run: npx prettier --check .

Порядок имеет значение. tsc --noEmit быстрее и даёт самые точные ошибки типов. ESLint — дольше (type-checked пресет требует language server). Prettier — быстро, но должен быть последним: нет смысла проверять форматирование кода с ошибками типов.

При любом нарушении — CI падает, PR не мержится. Исключений нет. Механизм «поправлю потом» не существует — tsc и ESLint накапливают ошибки линейно с ростом кодовой базы.

Локально перед коммитом — через lint-staged или pre-commit хук:

{
  "lint-staged": {
    "*.ts": ["tsc --noEmit", "eslint --fix", "prettier --write"]
  }
}

--fix на eslint и --write на prettier — только локально. В CI только --check.

@ts-expect-error — когда без него не обойтись

NODE-X1: @ts-ignore запрещён. Только @ts-expect-error с кодом ошибки и обоснованием.

function processOrder(order: Order): void {
  // @ts-ignore
  order.unknownField;
}

// @ts-expect-error TS2339 — сторонняя библиотека не экспортирует тип для поля,
// TODO: убрать после апгрейда @types/library до 3.x
const value = (response as any).__internalField;

Разница принципиальна. @ts-ignore подавляет любую ошибку на следующей строке навсегда — даже если ошибка исчезла, подавление остаётся. @ts-expect-error падает, если на следующей строке нет ошибки: это детектор устаревших подавлений.

В NestJS типичный случай — декораторы без строгой типизации в тестовом окружении:

@Injectable()
export class CreateOrderHandler {
  constructor(
    @Inject(ORDER_REPOSITORY)
    private readonly orderRepository: OrderRepository,
  ) {}

  async execute(command: CreateOrderCommand): Promise<OrderId> {
    const customer = await this.orderRepository.findCustomerById(
      command.customerId,
    );
    if (customer === undefined) {
      throw new CustomerNotFoundError(command.customerId);
    }
    const order = Order.create(customer, command.items);
    await this.orderRepository.save(order);
    return order.id;
  }
}

Здесь @ts-expect-error не нужен — noUncheckedIndexedAccess и strict сами не позволят обратиться к customer без проверки на undefined.

Отключение правил ESLint — только с обоснованием

NODE-X2: // eslint-disable без кода правила и обоснования запрещён.

// eslint-disable-next-line
const result = riskyOperation();

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- сторонняя библиотека
// возвращает unknown, типизация добавлена в следующем PR (TASK-1234)
const result: SomeType = riskyOperation();

Каждое отключение без обоснования — потенциальный вектор деградации типизации. Через полгода никто не помнит, почему правило было отключено. С обоснованием — видно, что это осознанный выбор с контекстом, а не «так сошлось».

Что запрещено

АнтипаттернПравилоЧто взамен
tsconfig.json без "strict": trueNODE-2"strict": true обязательно
"moduleResolution": "node" в новом сервисеNODE-2"moduleResolution": "NodeNext"
noUncheckedIndexedAccess отсутствуетNODE-2добавить явно — не входит в strict
Несколько .eslintrc в поддиректорияхNODE-3единый eslint.config.mjs в корне
ESLint без strictTypeChecked пресетаNODE-3tseslint.configs.strictTypeChecked
Отсутствие prettier --check в CINODE-4добавить шаг в pipeline
// @ts-ignoreNODE-X1// @ts-expect-error TS<код> — <обоснование>
// eslint-disable без кода правила и обоснованияNODE-X2eslint-disable-next-line <rule> -- <why>
Ослабление флагов strict без PR-обоснованияNODE-2точечный @ts-expect-error с TODO

Куда дальше

  • node/naming.md — kebab-case файлы, PascalCase классы, camelCase функции, UPPER_SNAKE_CASE константы.
  • node/imports.md — named exports, порядок импортов, path-aliases, import type, запрет барелей.
  • node/expressions.md — unknown + narrowing вместо any, guard clause, типизация публичных сигнатур.
  • node/async.md — async/await, Promise.all, запрет fire-and-forget.
  • node/immutability.md — readonly на полях, as const, spread вместо мутации аргументов.
  • Стандарты → Code Style — хаб языковых биндингов: Java, Node, Python, Go.