strictNullChecks и другие strict-опции
Вопрос с собеседования: вот функция и вызов, который в обычном JavaScript просто упал бы с ошибкой в рантайме. Что скажет TypeScript с включённым strict: true в tsconfig.json?
function getUserName(id: number): string | undefined {
const users: Record<number, string> = { 1: "Аня", 2: "Борис" };
return users[id];
}
function greet(id: number) {
const name = getUserName(id);
console.log(name.toUpperCase()); // ?
}Вывод (компилятор):
'name' is possibly 'undefined'.ts(18048)Без строгого режима этот код спокойно скомпилируется, а упадёт только в рантайме — если передать id, которого нет в users, получим undefined.toUpperCase() и классическую ошибку Cannot read properties of undefined. Со strict: true TypeScript отказывается компилировать код до того, как ты явно обработаешь случай undefined. Это и есть вся суть вопроса: строгий режим переносит целый класс рантайм-ошибок на этап компиляции, когда их дёшево исправить.
strict: true в tsconfig.json — это не одна опция, а переключатель сразу нескольких проверок одновременно. Самая заметная и обсуждаемая из них — strictNullChecks. Без неё null и undefined считаются совместимыми с любым типом: переменную типа string можно было присвоить как null, и компилятор промолчит. С strictNullChecks это поведение меняется: null и undefined становятся отдельными типами, которые нужно указывать явно, если значение действительно может их принимать.
// без strictNullChecks: тип string молча принимает null
// с strictNullChecks: нужно явно объявить union
function findUser(id: number): string | null {
return id === 1 ? "Аня" : null;
}
const user = findUser(2);
if (user !== null) {
console.log(user.toUpperCase()); // тут TypeScript уже знает: user — string
} else {
console.log("Пользователь не найден");
}Вывод:
Пользователь не найденИз этого правила следует то, что многих раздражает при первом знакомстве с TypeScript: обращение к полю объекта, который потенциально undefined, не пройдёт проверку без явной защиты — либо через if, как выше, либо через optional chaining ?., либо через non-null assertion ! (когда ты лично ручаешься компилятору, что тут значение точно есть). Раздражает это ровно до первого сбоя в проде, который strict-режим предотвратил бы ещё на этапе написания кода.
Кроме strictNullChecks, флаг strict: true включает ещё несколько важных проверок. noImplicitAny запрещает TypeScript молча выводить тип any там, где он не смог определить тип сам — например, для параметра функции без аннотации: без этой опции такой параметр тихо становится any, с ней — требует явного типа.
// с noImplicitAny: ошибка "Parameter 'x' implicitly has an 'any' type"
function double(x) {
return x * 2;
}
// исправлено:
function doubleFixed(x: number): number {
return x * 2;
}strictFunctionTypes делает более строгой проверку совместимости типов функций-параметров (это связано с понятием вариантности — насколько безопасно подставить функцию с одной сигнатурой туда, где ожидается другая). strictPropertyInitialization проверяет, что все поля класса получают значение либо при объявлении, либо в конструкторе — без неё можно объявить поле с типом string и ни разу не присвоить ему значение, и TypeScript это пропустит.
class UserProfile {
name: string; // ОШИБКА со strictPropertyInitialization: свойство не инициализировано
constructor() {
// забыли присвоить this.name
}
}Вывод (компилятор):
Property 'name' has no initializer and is not definitely assigned in the constructor.ts(2564)Есть в семействе strict ещё одна опция, которая напрямую связана с типами any и unknown из первого урока этого раздела, — useUnknownInCatchVariables. Она меняет тип переменной в блоке catch с any на unknown:
try {
JSON.parse("не json");
} catch (error) {
// без useUnknownInCatchVariables: error имеет тип any, error.message пройдёт без вопросов
// с useUnknownInCatchVariables: error имеет тип unknown, нужна проверка
if (error instanceof Error) {
console.log(`Ошибка парсинга: ${error.message}`);
} else {
console.log("Поймали не Error, а что-то другое");
}
}Вывод:
Ошибка парсинга: Unexpected token 'н', "не json" is not valid JSONЭто исправление не капризная строгость ради строгости: в JavaScript оператором throw можно выбросить буквально что угодно — не только объект Error, но и строку, число или произвольный объект. Код вроде throw "что-то пошло не так" синтаксически абсолютно легален. Если бы тип error в catch всегда был any, обращение error.message компилировалось бы без единой жалобы даже там, где реально поймана строка, а не объект ошибки — и упало бы в рантайме с попыткой прочитать поле message у строки. unknown в catch заставляет сначала проверить через instanceof Error (или другую проверку), прежде чем читать message — ровно та же логика «сначала докажи тип, потом используй», что мы разбирали в первом уроке раздела.
Как это работает под капотом. Все strict-проверки — это работа компилятора tsc на этапе анализа типов, ещё до генерации JavaScript-файла. Ни одна из них не остаётся в скомпилированном коде и не влияет на рантайм — итоговый JS выглядит одинаково что со strict: true, что без него (если код в принципе скомпилировался). Разница целиком в том, какой код компилятор согласится пропустить: строгий режим требует больше явных доказательств безопасности от программиста, а взамен ловит на несколько классов ошибок больше, причём именно на этапе разработки, а не у пользователя в браузере.
Частые ошибки на собеседовании: путают strict: true с одной конкретной опцией — на самом деле это семейство из восьми с лишним флагов (strictNullChecks, noImplicitAny, strictFunctionTypes, strictPropertyInitialization, strictBindCallApply, noImplicitThis, alwaysStrict, useUnknownInCatchVariables и другие), которые можно включать по отдельности. Ещё одна ошибка — считать, что strict-режим делает код медленнее: он влияет только на компиляцию, не на скорость выполнения. И третья — злоупотреблять non-null assertion (!) как способом «заткнуть» ошибку strictNullChecks вместо того, чтобы реально обработать случай отсутствия значения: это молча возвращает ровно те рантайм-риски, от которых strict-режим должен был защитить.