Опирается на правила:
NODE-2…NODE-5,NODE-X1,NODE-X2из Node Style Guide → раздел 1. Инструменты.
Важно знать
tsconfig.jsonдолжен содержать"strict": trueи"noUncheckedIndexedAccess": true;moduleиmoduleResolution—NodeNext.- 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 |
noImplicitThis | this вне класса без явного типа |
alwaysStrict | "use strict" во всех файлах |
useUnknownInCatchVariables | catch (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": true | NODE-2 | "strict": true обязательно |
"moduleResolution": "node" в новом сервисе | NODE-2 | "moduleResolution": "NodeNext" |
noUncheckedIndexedAccess отсутствует | NODE-2 | добавить явно — не входит в strict |
Несколько .eslintrc в поддиректориях | NODE-3 | единый eslint.config.mjs в корне |
ESLint без strictTypeChecked пресета | NODE-3 | tseslint.configs.strictTypeChecked |
Отсутствие prettier --check в CI | NODE-4 | добавить шаг в pipeline |
// @ts-ignore | NODE-X1 | // @ts-expect-error TS<код> — <обоснование> |
// eslint-disable без кода правила и обоснования | NODE-X2 | eslint-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.