Строгость tsconfig: что на самом деле включает `strict` и какие флаги он оставляет за бортом
`strict: true` переключает восемь флагов разом; `strictNullChecks` меняет тип каждого значения, отделяя null/undefined от `T`. Главные выигрыши — во флагах вне набора: `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes` — и в постепенной миграции слабо типизированной базы.
Сервис месяцами работал в dev. Потом коллега поставил "strict": true в tsconfig.json, чтобы причесать новый пакет, — и весь репозиторий вспыхнул 1400 ошибками, в основном Object is possibly 'undefined'. Никто не менял логику. Один флаг разом перетипизировал каждое nullable-значение в кодовой базе. Этот флаг — strictNullChecks, и strict включил его вместе с семью братьями.
strict — один переключатель, замкнутый на восемь
strict: true не добавляет ни одной отдельной проверки — он задаёт значение по умолчанию для семейства флагов, каждый из которых при этом можно переопределить индивидуально:
// tsconfig.json
{
"compilerOptions": {
"strict": true
// подразумевает, если не переопределить каждый:
// strictNullChecks, strictFunctionTypes, strictBindCallApply,
// strictPropertyInitialization, noImplicitAny, noImplicitThis,
// useUnknownInCatchVariables, alwaysStrict
}
}Все они меняют только проверку — ни один не меняет генерируемый JS (кроме alwaysStrict, который добавляет "use strict"). Они ужесточают то, что компилятор примет, обнажая баги, которые всегда были скрыты. Паттерн переопределения важен при миграции: можно включить strict, но выборочно ослабить самый громкий флаг.
{
"compilerOptions": {
"strict": true,
"strictNullChecks": false // временно; всё остальное остаётся строгим
}
}| флаг | что ловит | типичная первая реакция |
|---|---|---|
strictNullChecks | null/undefined там, где требуется ненулевое значение | тысячи possibly 'undefined' |
noImplicitAny | параметр/переменная, чей тип не удалось вывести и он упал в any | «добавь сюда аннотацию типа» |
strictFunctionTypes | небезопасная вариантность параметров колбэка (аргументы проверяются контравариантно) | обработчик типизирован слишком широко |
strictBindCallApply | .call/.apply/.bind вызваны с неверными типами аргументов | несоответствие this/аргументов |
strictPropertyInitialization | поле класса, которому никогда не присваивается значение в конструкторе | Property has no initializer |
useUnknownInCatchVariables | пойманная ошибка трактуется как any вместо unknown | err — это unknown, надо сузить |
noImplicitThis | this неявного типа any в свободной функции | аннотируй параметр this |
alwaysStrict | (emit) парсит файлы в strict-режиме + добавляет "use strict" | почти не виден |
strictNullChecks: тот, что перетипизирует всё
Без него null и undefined присваиваемы любому типу — string молча включает null. С ним они становятся собственными типами и должны моделироваться явно:
// strictNullChecks: true
function greet(name: string) {
return name.toUpperCase(); // безопасно: name не может быть null/undefined
}
const el = document.querySelector(".btn"); // Element | null
el.addEventListener("click", handler);
// ^^ Ошибка: 'el' is possibly 'null'. ts(18047)
// фикс: if (el) el.addEventListener(...) — или el?.addEventListener(...)Именно этот флаг создаёт стену из 1400 ошибок, потому что показывает каждое место, где старая система типов лгала. И это же — главный выигрыш по корректности во всём наборе.
▸Почему это работает
Почему он выключен по умолчанию вне strict? Потому что TypeScript вышел раньше, чем он появился (флаг пришёл в 2.0), и включение его на легаси-базе — нетривиальная миграция. Исходное поведение «всё nullable» — несостоятельное (unsound): оно пропускает null сквозь проверки типов. Поэтому strictNullChecks — это флаг, который заставляет систему типов действительно означать то, что она говорит.
Ценные флаги, которые strict НЕ включает
Почему важно знать, каких флагов нет в наборе? Потому что самые частые TypeScript-баги — выход за границы массива, отсутствующий ключ конфига, возвращающий undefined — живут именно в пробелах, которые набор strict не закрывает. Эти два ловят настоящие баги и не входят в набор strict — вы подключаете их отдельно:
{ "compilerOptions": { "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true } }noUncheckedIndexedAccess делает так, что любой доступ по индексу к массиву или record возвращает T | undefined, потому что индекс может выйти за границы, а ключ — отсутствовать:
// noUncheckedIndexedAccess: true
const xs = [1, 2, 3];
const first = xs[0]; // number | undefined (а не number!)
first.toFixed(); // Ошибка: 'first' is possibly 'undefined'.
const cfg: Record<string, string> = {};
const v = cfg.missing; // string | undefinedОн громкий, и почти каждая его жалоба — настоящий скрытый баг (off-by-one, отсутствующий ключ, доступ к пустому массиву). Цена — реальная церемония в телах циклов и при поиске; выигрыш в том, что «доступ к массиву не может быть undefined» перестаёт быть ложью.
exactOptionalPropertyTypes закрывает тонкую дыру. Без него { a?: string } и { a: string | undefined } считаются эквивалентными — то есть можно присвоить { a: undefined } «опциональному» свойству. С ним они расходятся: опциональное свойство может отсутствовать, но писать в него явный undefined нельзя.
// exactOptionalPropertyTypes: true
interface Opts { timeout?: number } // может ОТСУТСТВОВАТЬ, а не быть undefined
const a: Opts = {}; // ок — отсутствует
const b: Opts = { timeout: undefined }; // Ошибка: undefined не присваиваем;
// timeout? значит «отсутствует», а не «есть и undefined»
const c: Opts = { timeout: 200 }; // окЭто важно, когда код делает "timeout" in opts или Object.keys(opts): при слабом дефолте эти ветки ведут себя неверно, потому что { timeout: undefined } имеет ключ.
Расставь шаги миграции большой слабо типизированной базы к полной строгости без «большого взрыва» из 1400 ошибок:
- 1 Включи strict: true, но сразу поставь strictNullChecks: false, чтобы билд оставался зелёным
- 2 Сначала почини дешёвых братьев (noImplicitAny, strictBindCallApply) — мало ошибок, всё локально
- 3 Переключи strictNullChecks: true и чини стену null/undefined пофайлово (или по каталогам)
- 4 Добавь noUncheckedIndexedAccess и exactOptionalPropertyTypes последними — они обнажают самые глубокие скрытые баги
При noUncheckedIndexedAccess: true каков тип arr[0], где const arr = [1, 2, 3]?
- 01Что на самом деле делает `strict: true` и какие восемь флагов он подразумевает?
- 02Почему strictNullChecks — самый значимый флаг и что меняется, когда он включён?
- 03Что ловят noUncheckedIndexedAccess и exactOptionalPropertyTypes и почему их нет в наборе strict?
Теперь ты знаешь, что strict — мета-флаг, замыкающий восемь проверок, что strictNullChecks — тот, кто перетипизирует каждое значение, и что noUncheckedIndexedAccess и exactOptionalPropertyTypes — opt-in флаги, где прячутся самые глубокие баги. Дальше — разрешение модулей: когда проверки типов прошли, компилятору всё ещё нужно найти файлы и пакеты, которые ты импортируешь; неверный moduleResolution — классическая ловушка «работает в редакторе, падает в рантайме». Теперь, когда видишь стену possibly 'undefined' после чьего-то strict: true, ты знаешь, какой флаг сработал — и какие opt-in флаги добавить, когда пыль уляжется.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.