"strict": true — это базовый минимум, а не максимум. На реальных проектах мы включаем десяток дополнительных флагов, которые ловят классы багов на этапе компиляции. Расскажу, что включаем и почему.
strict: что входит и что не входит
strict: true включает 8 флагов: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict. Это база.
Но есть ещё несколько важных флагов, которые в strict не входят. И именно они ловят самые неприятные баги.
noUncheckedIndexedAccess
Без этого флага array[i] имеет тип T, даже если i за пределами массива. С флагом — тип становится T | undefined, и компилятор заставляет проверить.
const items = ["a", "b", "c"];
const item = items[5]; // без флага: string. С флагом: string | undefined
Это ловит реальные runtime-баги. На крупных проектах включение флага вскрывает 50-200 мест с потенциальными ошибками. Включайте сразу.
exactOptionalPropertyTypes
Без флага { name?: string } принимает { name: undefined }. С флагом — нет, нужно либо не указывать поле, либо указать string.
type User = { name?: string };
const u: User = { name: undefined }; // флаг отлавливает
Это спасает от багов с тонкой разницей между «поле отсутствует» и «поле есть, но значение undefined». Особенно важно при работе с базой и API.
noImplicitOverride
Когда вы переопределяете метод базового класса, TS не требует override. С флагом — требует. Это спасает от ситуации, когда вы переопределили метод, потом в базе его переименовали, а ваш переопределённый метод стал «новым», и базовый поломался.
В функциональных проектах без классов — флаг бесполезен. В проектах с инхеритансом (NestJS, классы для модельных слоёв) — обязателен.
noFallthroughCasesInSwitch
Любой case в switch без break или return становится ошибкой. Это ловит самый неприятный класс багов, когда забыли break и case проваливается в следующий.
Параметры функций и any
noImplicitAny — часть strict. Но иногда команда пишет явные : any — флагом не поймаешь. Добавьте ESLint-правила @typescript-eslint/no-explicit-any. Не запретит совсем (есть редкие места, где any оправдан), но требует явного отключения через комментарий.
unknown лучше any в 99% случаев. Если приходят неизвестные данные — unknown плюс Zod-валидация в точке входа.
Миграция legacy-проекта
Если у вас старый проект без strict — не включайте всё разом. Стратегия:
- Включить
strict: true, посмотреть количество ошибок. - Если их сотни — включать по одному флагу из strict (
strictNullChecksобычно самый болезненный). - Использовать
// @ts-expect-errorс комментарием на временные обходы. Так вы видите все долги в одном grep. - Постепенно чистить
@ts-expect-error, начиная с наиболее критичных модулей.
На проекте 50 000 строк это занимает 2-4 недели чистого времени.
Конфиг tsconfig.json
Базовый вариант для нового проекта:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true
}
}
verbatimModuleSyntax: true форсит явное import type для типов. Это убирает класс багов с циклическими зависимостями и ускоряет билд.
Проверка в CI
tsc --noEmit — обязательный шаг в CI. Локально разработчик может игнорировать ошибки, но в pull request их видно сразу.
В Next.js вызывается через next lint и tsc --noEmit. В монорепо удобно через Turbo: turbo run type-check.
Итого
TypeScript «на всю катушку» — это семь-восемь флагов сверх strict, плюс правильный ESLint, плюс CI. Включение даёт реальное снижение runtime-багов на 30-50% на сложных проектах. Затраты — пара недель на миграцию legacy-кода. Окупается с первого пойманного бага в проде.
Частые вопросы
Что входит в strict mode TypeScript и достаточно ли его?
strict: true включает 8 флагов: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict. Это база, а не максимум. Есть несколько важных флагов, которые в strict не входят: noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noFallthroughCasesInSwitch, noImplicitReturns. Именно они ловят самые неприятные баги в реальных проектах.
Зачем включать noUncheckedIndexedAccess?
Без этого флага array[i] имеет тип T, даже если i за пределами массива. С флагом — тип становится T | undefined, и компилятор заставляет проверить. Это ловит реальные runtime-баги. На крупных проектах включение флага вскрывает 50-200 мест с потенциальными ошибками. Включайте сразу для нового проекта. Для legacy постепенно через @ts-expect-error в проблемных местах. Один из самых мощных флагов для предотвращения undefined-ошибок в проде.
Что даёт exactOptionalPropertyTypes?
Без флага { name?: string } принимает { name: undefined }. С флагом — нет, нужно либо не указывать поле, либо указать string. Это спасает от багов с тонкой разницей между «поле отсутствует» и «поле есть, но значение undefined». Особенно важно при работе с базой и API, где undefined и отсутствие поля имеют разный смысл (например, в JSON-сериализации). Без этого флага легко получить неожиданное поведение при сравнении или сериализации объектов.
Как мигрировать legacy-проект на строгий TypeScript?
Стратегия. Включить strict: true, посмотреть количество ошибок. Если их сотни — включать по одному флагу из strict (strictNullChecks обычно самый болезненный). Использовать // @ts-expect-error с комментарием на временные обходы. Так вы видите все долги в одном grep. Постепенно чистить @ts-expect-error, начиная с наиболее критичных модулей. На проекте 50 000 строк это занимает 2-4 недели чистого времени. Не пытайтесь сделать всё разом — будет конфликтов в Git и blocked PRs.
Какой tsconfig.json использовать для нового проекта?
Базовый набор флагов для нового проекта. strict: true. noUncheckedIndexedAccess: true. exactOptionalPropertyTypes: true. noImplicitOverride: true. noFallthroughCasesInSwitch: true. noImplicitReturns: true. forceConsistentCasingInFileNames: true. verbatimModuleSyntax: true. verbatimModuleSyntax форсит явное import type для типов — убирает класс багов с циклическими зависимостями и ускоряет билд. Это конфигурация, которую мы используем на всех новых проектах с первого дня.
Как настроить TypeScript-проверку в CI?
tsc --noEmit — обязательный шаг в CI. Локально разработчик может игнорировать ошибки, но в pull request их видно сразу. В Next.js вызывается через next lint и tsc --noEmit. В монорепо удобно через Turbo: turbo run type-check. Также добавьте ESLint-правила @typescript-eslint/no-explicit-any (не запретит совсем, но требует явного отключения через комментарий). unknown лучше any в 99% случаев. Если приходят неизвестные данные — unknown плюс Zod-валидация в точке входа.