strict изнутри: какие проверки он включает

настройка≈ 10 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Строка "strict": true в конфиге выглядит как один переключатель, но за ней стоят восемь независимых проверок — у каждой своё имя, свой код диагностики и свой год появления. Разбирать их по одной приходится ровно тогда, когда одну из них надо выключить, а остальные оставить.

Разделы темы:·

strict сам по себе ничего не находит. Это выключатель, который поднимает восемь других флагов разом — примерно как --all у консольной утилиты. Пока всё включено, разница между ними незаметна; она проявляется в первый же день, когда команда решает «вот это временно отключить».

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Два TS7006 на параметрах w и h — это noImplicitAny. TS2683 на this — noImplicitThis. TS2322 на null — strictNullChecks. TS18046 на err.message — useUnknownInCatchVariables. Пять ошибок в четырёх строчках, и ни одна не мешает коду выполниться: диагностики напечатались, а console.log всё равно отработал.

ФлагЧто перестаёт быть нормойДиагностика
noImplicitAnyпараметр или свойство, тип которого неоткуда вывестиTS7006, TS7031
noImplicitThisthis в функции без объявленного получателяTS2683
strictNullChecksnull и undefined внутри обычных типовTS2322, TS18047, TS18048
strictFunctionTypesфункция с более узким параметром на месте более широкойTS2322
strictBindCallApplycall, apply и bind с любыми аргументамиTS2345
strictPropertyInitializationполе класса, которому не гарантировано присваиваниеTS2564
useUnknownInCatchVariablesпойманная ошибка как заведомый ErrorTS18046
strictBuiltinIteratorReturnvalue встроенного итератора как всегда существующийTS18048

Начиная с шестой версии компилятора этот выключатель поднят по умолчанию. Проверяется за минуту: положите рядом файл с function area(w, h) и tsconfig.json, в котором про строгость нет ни слова, — tsc --noEmit всё равно ответит TS7006. Усилие теперь требуется в обратную сторону: чтобы проверок не было, нужно написать "strict": false. Шаблон из tsc --init по-прежнему пишет "strict": true явно — так конфиг честнее читается человеком.

Любой из восьми флагов перебивает strict в обе стороны и независимо от порядка строк. "strict": true вместе с "strictNullChecks": false оставит семь проверок из восьми; "strict": false вместе с "noImplicitAny": true — ровно одну. Именно так и переезжают старые проекты: включают по флагу за подход.

Девятым в семье когда-то был alwaysStrict — тот, что дописывает "use strict" в результат сборки. Он ушёл: теперь включён всегда, а на "alwaysStrict": false шестая версия отвечает TS5107 и обещает убрать эту возможность в седьмой.

TS7006 из главы про базовые типы — самая частая ошибка новичка в TypeScript, и обычно её понимают шире, чем следует: будто noImplicitAny запрещает любую переменную без аннотации. Это не так, и граница проходит в неожиданном месте.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

На объявлении let value; компилятор промолчал, хотя типа там нет. Переменная без аннотации и без начального значения получает «развивающийся» any: компилятор следит за присваиваниями и в каждой точке знает текущий тип. Поэтому первый вызов законен, а второй — уже TS2551: value к тому моменту строка, и toFixed у неё нет. Запускаете — и получаете тот же вывод от движка: 10.0, а следом TypeError.

Неявный any запрещён там, где следить не за чем. У параметра функции значение приходит снаружи, из мест, которых компилятор не видит, — вот его он и требует подписать. Второй такой источник — this.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Отдельно стоящая tick даже не вызвана — TS2683 появляется на объявлении. Компилятору неоткуда узнать, кем окажется this: в части I мы видели, что это решается в момент вызова. Способ сказать ему это явно — фиктивный первый параметр с именем this, который в результат сборки не попадает.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Теперь this типизирован, и вызов без подходящего получателя компилятор тоже не пропустит — та самая потеря контекста из главы про this, но пойманная до запуска.

До версии 4.4 переменная в catch имела тип any, и обращаться к её полям можно было как угодно. Флаг меняет тип по умолчанию на unknown — со всеми последствиями из главы про две двери из системы типов.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Пример показателен тем, что TS18046 здесь выглядит придиркой: JSON.parse действительно бросает SyntaxError, и после диагностики код печатает нормальное сообщение. Придирка кончается на первой чужой библиотеке, которая бросает строку или объект без message. throw в JavaScript принимает что угодно, поэтому unknown — единственный честный тип для пойманного значения.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Одна функция-разборщик на приложение снимает вопрос навсегда: сужение через instanceof внутри неё написано один раз, а catch в коде остаётся однострочным.

Флаг меняет только тип по умолчанию. Явная аннотация в catch разрешена и сегодня, причём допустимы ровно два типа — any и unknown.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Это лучше, чем выключать флаг на весь проект: any виден в месте, где им воспользовались, и его найдёт поиск по репозиторию.

strictNullChecks разобран отдельно в главе про сужение типов — он самый дорогой при переезде и самый полезный после. Здесь важно другое его свойство: он несущая стена, на которой стоят соседи.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS2564 — работа strictPropertyInitialization, и предупреждение оказывается точным: на исполнении guest.token равен undefined, и чтение length валит программу. Способов честно закрыть три: объявить token: string | undefined, задать значение по умолчанию или, если присваивание гарантировано вне конструктора, поставить восклицательный знак — token!: string. Подробности — в статье про TS2564 из главы о классах.

А теперь связь. Попробуйте включить strictPropertyInitialization без strictNullChecks — компилятор откажется собирать проект: «Параметр "strictPropertyInitialization" невозможно указать без указания параметра "strictNullChecks"», TS5052. Это логично: без учёта null и undefined тип string и так включает их обоих, проверять нечего.

Обратный порядок ловушку и создаёт. Запись "strict": true вместе с "strictNullChecks": false никакого TS5052 не даёт: конфиг собирается, а TS2564 из примера выше просто исчезает. Один выключенный флаг молча уносит второй.

Поэтому «выключим только strictNullChecks на время миграции» стоит дороже, чем кажется: вместе с ним из проекта уходит и проверка полей класса, и половина смысла остальных шести.

Эти два флага почти не попадаются в учебных примерах, зато срабатывают в живом коде с обработчиками. Начнём с того, который ловит настоящую ошибку подстановки.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS2322 с вложенной причиной: «Свойство "bark" отсутствует в типе "Animal"». Проигнорируйте её — и на исполнении получите TypeError: d.bark is not a function. Правило звучит наоборот тому, чего ждёт интуиция: на место функции, принимающей Animal, годится функция, принимающая больше вариантов, а не меньше. Сужать параметр небезопасно — вызывающая сторона имеет право прислать кота.

А теперь то, из-за чего этот флаг стоит знать по имени. Один и тот же обработчик, записанный двумя способами, проверяется по-разному.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностика одна — на byProperty. Совершенно такое же присваивание строкой выше компилятор пропустил, и падает программа именно на нём. Это не баг: strictFunctionTypes сознательно не распространяется на свойства, записанные синтаксисом метода. Иначе развалилась бы стандартная библиотека — массив собак перестал бы быть массивом животных, потому что push(item: Dog) и push(item: Animal) объявлены методами.

Хотите, чтобы обработчик в интерфейсе проверялся честно, — объявляйте его полем со стрелочным типом, а не методом. Разница в одном двоеточии и в целом классе пойманных ошибок.

Второй флаг проще. До его появления call, apply и bind были объявлены через any[], и аргументы в них не проверялись вообще.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS2345 на второй строке — и обратите внимание, чем оборачивается её игнорирование: не падением, а пустой строкой. String.prototype.repeat приводит "два" к NaN, а NaN к нулю. Ошибка, которая не бросается, а тихо портит вывод, — как раз тот случай, ради которого систему типов и заводят. bind с частичным применением проверяется так же строго.

Флаг появился в версии 5.6 и сразу вошёл в strict — поэтому при обновлении компилятора он ломает сборку в проектах, которые ничего в конфиге не меняли. Речь о ручном обходе встроенных итераторов.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS18048: «Возможно, "extra.value" имеет значение undefined». Раньше value у встроенных итераторов был просто number, и такой код собирался молча — а на исполнении давал ровно то, что вы сейчас увидели после диагностики. Флаг привёл типы в соответствие со стандартом: у исчерпанного итератора value действительно undefined.

Обычный for..of флаг не трогает — цикл сам смотрит на done и до пустого результата не добирается. Ломается только код, который дёргает next() руками, а таких мест в проекте обычно единицы.

Чаще всего — попытка достать «первый элемент» коллекции, минуя цикл.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Лечится проверкой или переходом на то, что не врёт про пустоту: [...map.entries()][0] с проверкой длины либо обычный цикл с break.

Общий вывод по всем восьми: strict — не позиция «всё или ничего». Если какая-то проверка сейчас неподъёмна, честнее оставить "strict": true и выключить один флаг строкой ниже, с комментарием и датой, чем написать "strict": false и потерять восемь проверок вместо одной. Тем более что теперь по умолчанию они включены — усилие требуется, чтобы от них отказаться.

  • strict — не проверка, а выключатель восьми: noImplicitAny, noImplicitThis, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, useUnknownInCatchVariables и strictBuiltinIteratorReturn.
  • С шестой версии компилятора он поднят по умолчанию: теперь усилие требуется, чтобы проверок не было, а не чтобы их получить.
  • Отдельный флаг перебивает strict в обе стороны — этим и пользуются при переезде старого проекта, включая проверки по одной.
  • strictNullChecks несущий: без него молча отключается и проверка инициализации полей, а включить её отдельно компилятор не даст (TS5052).
  • strictFunctionTypes не проверяет свойства, записанные синтаксисом метода, — эта дыра оставлена сознательно, ради совместимости стандартной библиотеки.
  • Выключать один флаг с комментарием честнее, чем писать "strict": false и терять восемь проверок разом.

1/4В конфиге написано "strict": true, а строкой ниже "strictNullChecks": false. Что произойдёт с проверкой инициализации полей класса (TS2564)?

2/4Интерфейс объявлен как interface AsMethod { handle(a: Animal): void }. Ему присваивают объект, у которого handle принимает более узкий Dog. Что скажет компилятор со включённым strict?

3/4Строка let value; без аннотации и без начального значения — что на неё скажет noImplicitAny?

4/4Что случится, если запустить tsc --noEmit шестой версии на файле с function area(w, h) и конфигом, где про strict не сказано ни слова?

Назвать виноватого

Компилятор со включённым strict находит в этом коде четыре ошибки. Для каждой определите, какой из восьми флагов её включил, — а потом запустите и сверьтесь с диагностиками.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

По порядку строк:

  • total: number — TS2564, флаг strictPropertyInitialization: при пустом массиве присваивания не будет.
  • параметр cart — TS7006, флаг noImplicitAny.
  • this.total в report — TS2683, флаг noImplicitThis: у функции нет объявленного получателя.
  • err.message — TS18046, флаг useUnknownInCatchVariables.

Заодно обратите внимание, что программа при этом отработала: describe вернул строку с undefined вместо суммы. Ошибка из тех, что доезжают до продакшена в виде «сумма: undefined» на странице заказа.

Разбор ошибки без any

Функция должна вернуть число из JSON, а при неудаче — напечатать причину и вернуть 0. Сейчас компилятор даёт TS18046. Почините так, чтобы диагностик не осталось, а any в коде не появился — ни явным, ни через утверждение типа.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Пойманное значение имеет тип unknown, и это правда: бросить в JavaScript можно что угодно. Значит, перед чтением message нужно сузить тип — тем же instanceof, что и в главе про сужение.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Ветка со String(err) не формальность: именно она отработает, когда чужая библиотека бросит строку или объект без message.

Обработчик, который не упадёт

Тип Listener обещает, что обработчик примет любое животное, а написанный обработчик умеет только собак — отсюда TS2322 и падение на исполнении. Перепишите так, чтобы код собирался без диагностик и работал с обоими вызовами: кот должен «молчать», собака — лаять.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Сужать параметр нельзя — вызывающая сторона имеет полное право прислать кота. Значит, обработчик должен принимать Animal и разбираться с вариантами внутри. Проверку удобно вынести в предикат из главы про сужение типов.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Тип параметра у стрелки можно не писать вовсе: он выводится из Listener. Это тот же приём «не дублировать то, что уже сказано типом», что и в главе про вывод типов.