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-режим должен был защитить.

Проверьте себя
1. Что меняет опция strictNullChecks в поведении TypeScript?
AЗапрещает использовать null и undefined в коде вообще
BДелает null и undefined отдельными типами, которые нужно указывать явно, а не считать совместимыми с любым типом по умолчанию
CАвтоматически заменяет все null на undefined
DУскоряет компиляцию за счёт пропуска проверок
2. Как strict-проверки TypeScript влияют на итоговый скомпилированный JavaScript-код?
AДобавляют в код рантайм-проверки типов перед каждой операцией
BНикак не влияют на итоговый JS — вся строгость применяется только на этапе компиляции, влияя лишь на то, какой код компилятор согласится пропустить
CЗамедляют выполнение кода в браузере
DАвтоматически оборачивают весь код в try/catch