В прошлой статье я рассказал о различных особенностях некоторых общих настроек TypeScript. В данной статье речь пойдёт о так называемых «флагах строгости». На самом деле, TypeScript из коробки мало чем отличается от JavaScript. Поэтому если изначально не подтюнить конфиг проекта, то большая часть преимуществ языка не будет задействована. Смысл использования TypeScript в таком виде есть, но небольшой. Если выразить смысл всей статьи в одном предложении, то текст можно свести к следующему: откройте свой tsconfig.json, установите флаг strict в секции compilerOptions в значение true. Без режима строгости использование TypeScript это практически пустая трата времени. Однако цель предыдущей и данной статей заключается не в том, чтобы сказать как «правильно» делать, а прояснить некоторые тонкости конфигурации. Поэтому погрузимся в детали данного вопроса. В официальном референсе tsconfig.json его многочисленные опции поделены на секции. Из них две секции: Strict Checks и Linter Checks – содержат только опции тех самых флагов строгости. Помимо ещё часть интересующих нас сегодня флагов сокрыта в самой большой группе опций Advanced. Пожалуй, флаги данной категории наиболее важные из всех. Вот они: strict, alwaysStrict, noImplicitAny, strictNullChecks, strictFunctionTypes, strictPropertyInitialization, noImplicitThis, strictBindCallApply.  Все вышеперечисленные флаги по умолчанию имеют значение false, а для того, чтобы было строго нужно установить противоположное значение – true. Первое, что здесь может броситься в глаза, глядя на список опций, это наличие флагов с похожими именами, которые сбивают с толку - strict и alwaysStrict. На самом деле флаги выполняют совершенно разные функции. Рекомендован: всегда / сложность: легко. Флаг alwaysStrict включает добавление строки "use strict" в каждый скомпилированный файл. Другими словами, alwaysStrict включает строгий режим JavaScript и никак не связан с проверкой типов TypeScript. Рекомендован: всегда / сложность: сложно / может быть заменён набором других флагов. Флаг strict напрямую связан с проверкой типов. Его включение автоматически активирует абсолютно все флаги секции Strict Checks, включая и alwaysStrict. Это именно то, о чём я говорил в самом начале. У такого подхода есть как минимум один недостаток – неочевидность. Устанавливая strict: true, нет наглядного представления, какие именно проверки включены и какие опции вообще существуют. Для проектов, которые с самого начала пишутся на TypeScript это не так принципиально, как для проектов, которые поэтапно портируются с JavaScript. В процессе портирования существующего приложения нет возможности сразу включить все проверки. Приходится активировать их по одной шаг за шагом. Иногда даже случаются сложности, из-за которых приходится откатывать ранее установленные флаги обратно в false. Есть небольшая особенность работы флага strict – список подконтрольных ему флагов может пополняться по мере выхода новых версий TypeScript. Подобные моменты если случаются, то редко и всегда освещаются в release notes если, конечно, вы их читаете перед обновлением версии. Лично я предпочитаю указывать список флагов явным образом: Рассмотрим, что делает каждый из флагов и обсудим какие из них стоит включить в первую очередь, если активировать сразу все не получается. Рекомендую: всегда / сложность: средне Это первый флаг, который необходимо активировать в начале работы над проектом с TypeScript. Для начала несколько слов про any. Это специальный тип, который назначается всем переменным, если их тип не был задан явно и не может быть выведен компилятором автоматически. Данный тип создан для обратной совместимости с JavaScript. С точки зрения TypeScript, все переменные в JavaScript это any, поскольку в нём нет системы типов. Для компилятора тип any означает «это может быть что угодно, передаю управление и ответственность разработчику». Так как компилятор не знает тип переменной, с такой переменной можно делать всё что угодно. В этом месте TypeScript полностью лишён своей силы: Иначе говоря, если тип переменной не определён, значит он any, значит это JavaScript, а не TypeScript код. Т. е. количество неопределённых типов в коде, является реальным показателем отношения TypeScript кода к JavaScript коду. Флаг noImplicitAny подсвечивает все такие места, для того чтобы разработчик не забывал указывать типы. Однако это не означает, что теперь вовсе нельзя использовать any. Это означает лишь, что в подобных ситуация разработчик должен явно писать any, если по каким-то причинам не получается правильно описать тип переменной или если указание типа переменной приводит к ошибкам в другом участке кода. Явное присутствие any напоминает разработчику о том, что код не доработан и эти места должны быть улучшены со временем. В этом заключается суть флага noImplicitAny. Дополнительно усилить эффект можно включив ESLint правило no-explicit-any. Правило будет отмечать места с any как warning, каждый раз привлекая внимание разработчика. Рекомендую: всегда / сложность: средне Флаг активирует одну из самых мощных особенностей TypeScript. Рекомендую включить его сразу после noImplicitAny. JavaScript содержит два нижних значения – undefined и null, для которых в TypeScript есть одноимённые типы. По иерархии считается, что все остальные типы происходят от них. Таким образом по принципу наследования вместо переменной любого типа: string, boolean, number и т. д – можно передать значение undefined или null: Подобное поведение справедливо для почти всех (если не всех) языков программирования. На практике это может вызывать неудобства в виде написания дополнительных проверок на null (undefined). Но гораздо большей проблемой является помнить о необходимости покрывать код данными проверками. Думаю, Java разработчики с их NullPointerException как никто другой понимают эту проблему. Включение опции strictNullChecks меняет правила игры. С данным флагом компилятор больше не позволяет передавать undefined или null туда, где ожидаются переменные других типов, если это не разрешено явно. Рассмотрим пример: Это позволяет не обрабатывать ситуации с undefined и null в тех участках кода, где возникновение данных значений невозможно. При этом в тем местах, где возникновение подобных ситуаций возможно, компилятор не позволит забыть о проверках. Это очень сильный механизм, существование которого сложно переоценить. Есть только один потенциально опасный момент. В нашем статически типизированном коде всё будет работать как часы. Однако приложения часто работают с динамически получаемыми данными (например, данными от сервера), типы которых могут быть описаны одним образом, а на практике всё может быть совсем иначе. Другими словами, в типах описано, что некоторое значение не может быть null, а сервер его пришлёт. Избежать подобной ситуации можно введя практику проверять ответы от сервера. Например, с помощью json-схем валидаторов. Чуть более коварной может быть ситуация с внешними библиотеками. В частности, с теми, который изначально написаны на JavaScript и тайпинги для них существуют отдельно от кода. В таких случаях потенциальные несовпадения реального кода с типами могут привести к возникновению вышеописанной ситуации. Здесь можно найти таблицу совместимости типов с включенным и выключенным режимом strictNullChecks – Any, unknown, object, void, undefined, null and never assignability. Рекомендую: всегда / сложность: легко / связан с strictNullChecks Флаг strictPropertyInitialization следит, чтобы объявленные свойства класса всегда были инициализированы: Опция является своего рода дополнением к флагу strictNullChecks поэтому работает только когда второй флаг тоже включен. Рекомендую: всегда / сложность: легко Установка флага strictFunctionTypes: true включает более строгую проверку сигнатур функций. В целом, данная опция выглядит достаточно обязательной для использования: Рекомендую: всегда / сложность: легко При использовании this проверяет, что контекст выполнения известен. Рассмотрим такой пример: Объект this не известен, так как function не пробрасывает контекст автоматически. В данном случае это можно исправить, заменив function на arrow function. Рассмотрим пример независимой функции, которая по каким-то причинам должна использовать внешний this: В данном случае this также неизвестен. Выполнить функцию с внешним this можно с помощью bind, call, apply. А научить функцию понимать контекст можно следующим образом: Во всех случаях тип контекста известен и даже тайпхинтинг работает как следует. Рекомендую: всегда / сложность: средне Флаг strictBindCallApply – включает более строгую проверку сигнатур при использовании соответствующих методов: bind, call, apply. Кроме некоторых специфичных ситуаций при работе с данным флагом проблем не возникает. В случае возникновения сложных ситуаций рекомендую выключить проверку в месте возникновения проблемы через комментарий // @ts-ignore вместо полного отключения правила. здесь действительно все флаги очень полезные. С ними TypeScript раскрывает свой потенциал и приносит максимум пользы все флаги можно включить разом через strict: true или каждый по отдельности. Второй вариант отлично подходит для смешанных кодовых баз, где часть кода ещё не переведена на TypeScript. alwaysStrict не про строгий режим TypeScript, а про строгий режим JavaScript. Т. е. про "use strict". флаги первоочерёдной важности это noImplicitAny и strictNullChecks в сочетании с strictPropertyInitialization. Затем идут strictFunctionTypes и noImplicitThis. А напоследок strictBindCallApply. Название группы говорит само за себя – подразумевается, что флаги этой группы проверяют не соответствие типов, а качество кода. Своего рода правила как в ESLint. И действительно некоторые правила секции Linter Checks можно заменить на аналогичные правила ESLint. Полный список флагов данной секции: Все вышеперечисленные флаги по умолчанию имеют значение false, а для того, чтобы было строго нужно установить противоположное значение – true. Я неспроста отделил флаги noPropertyAccessFromIndexSignature и noUncheckedIndexedAccess пустой строкой от других флагов группы. Дело в том, что они находятся здесь только по причине того, что концептуально не могут располагаться в группе Strict Checks. По задумке в этой секции находятся флаги, которые подконтрольны опции strict. Но эти флаги слишком специфичные для того, чтобы включать их по умолчанию. Практически так и сказано в анонсах noPropertyAccessFromIndexSignature и noUncheckedIndexedAccess. Однако это не делает их правилами линтинга, они проверяют типы и по факту относятся к Strict Checks, только не включаются автоматически при включении флага strict. Остальные же флаги группы полноправно находятся в этой секции и имеют аналогичные правила в ESLint. В некоторых случаях с временными ограничениями, так как ещё не все правила перенесены в ESLint из TSLint. Соответствия правил можно посмотреть на странице TSLint Migration Guide и в репозитории ESLint rules for TSLint. Рассмотрим работу каждого из флагов. Рекомендую: всегда / сложность: легко / есть нюанс / нельзя заменить линтером Флаг noPropertyAccessFromIndexSignature запрещает обращаться к свойствам объекта через точку aka dot notation, если свойства объекта описаны не явно, а через произвольные параметры (aka arbitrarily-named properties, index signatures). Вместо точечной нотации мы используем скобочную нотацию aka bracket notation. Идея флага примерно такая же как у noImplicitAny - привлечь внимание к потенциально проблемным местам. Единственной причиной не использовать данную опцию – наличие двух видов синтаксиса для обращения к свойствам объекта. Однако на мой взгляд в этом нет проблемы так как хоть синтаксис и разный, у каждого из них есть своё назначение. Всегда используем dot notation и, если случается исключение, переходим на скобочную нотацию. При этом код становится безопаснее. Поэтому я рекомендую использовать этот флаг. На самом деле есть ещё одна причина, но она решается флагом noUncheckedIndexedAccess. Рекомендую: по ситуации / сложность: средне / есть серьёзный нюанс / связан с strictNullChecks / нельзя заменить линтером Ещё раз взглянем на предыдущий пример. Как мы видим, тип переменной username является string. И это не совсем правильно так как мы можем обратиться к любому свойству, даже если его не существует. Можно описывать произвольные поля следующим образом:  [key: string]: string | undefined. Это решит проблему, но добавит ручной работы. Плюс, из-за человеческого фактора можно периодически забывать дописывать undefined. Флаг noUncheckedIndexedAccess сделает эту работу за нас. Теперь компилятор не позволит использовать полученные таким образом переменные без предварительной проверки. Рассмотрим ещё один пример. На этот раз с массивом: Однако очевидно, что в данном случае переменная будет undefined. С включенным noUncheckedIndexedAccess в подобных ситуациях тип переменной будет определяться как string | undefined. Казалось бы, мы подчеркнули скобочной нотацией «магические» свойства объекта, мы можем использовать их только после проверок на undefined – всё красиво и безопасно. Что может пойти не так? Если кратко: Или другой пример: Другими словами, при данной конфигурации в обмен на безопасность в некоторых случаях придётся платить проверками, которые не несут смысла только для того, чтобы укротить строптивый компилятор. Выглядит это крайне костыльно. Определённо код с noUncheckedIndexedAccess становится значительно безопаснее, а это главная цель использования TypeScript. Однако компромисс может выглядеть достаточно серьёзным, поэтому я не могу рекомендовать флаг всем разработчикам. Флаг является своего рода дополнением к флагу strictNullChecks поэтому работает только когда второй флаг тоже включен. Рекомендую: всегда / сложность: легко / правило ESLint: consistent-return, пока не реализовано Флаг проверяет, чтобы все ветки функции возвращали значение: Аналогично правилу ESLint consistent-return, которое в данный момент ещё не реализовано для TypeScript. Рекомендую: если не используется ESLint / сложность: легко / правило ESLint: no-fallthrough Флаг проверяет наличие break в операторе switch/case: Заменяется правилом no-fallthrough. Рекомендую: для production если не используется ESLint / сложность: легко / правило ESLint: no-unused-vars Код проверяется на наличие неиспользуемых переменных: Данное правило крайне неудобно при использовании в процессе разработки. В процессе написания кода он может находится в «разобранном» состоянии, а наличие неиспользуемых переменных будет вызывать ошибку компиляции. Можно переопределять флаг для development окружения или использовать правило ESLint no-unused-vars. Рекомендую: для production если не используется ESLint / сложность: легко / правило ESLint: no-unused-vars Код проверяется на наличие неиспользуемых аргументов функций и методов: Данное правило также как noUnusedParameters мешает удобному процессу разработки. Можно переопределять флаг для development окружения или добавить дополнительные параметры к проверке ESLint no-unused-vars. Подведение итогов Linter Checks нет единого флага, которым можно активировать все опции секции опции noPropertyAccessFromIndexSignature и noUncheckedIndexedAccess по факту относятся к Strict Checks, только не активируются автоматически при включении главного флага strict остальные флаги секции: noFallthroughCasesInSwitch, noUnusedLocals, noUnusedParameters можно заменить правилами линтера. Только правило для noImplicitReturns временно не поддерживается Это самая большая секция и кажется, что в неё поместили все опции, которым не нашлось отдельных групп. Из них часть опций относится к настройкам строгости компилятора. Вот они: Первые два флага allowUnreachableCode и allowUnusedLabels опять отделены пустой строкой не случайно – они отбились от Linter Checks. Внезапно по умолчанию эти флаги имеют значение true, однако для строгости должны быть false – ровно наоборот. Запомнить это поведение достаточно легко. У предыдущих опций такого рода в названиях фигурировал префикс no, т. е. «запретить допущения». У данных же флагов префикс allow – «разрешить допущения». По-хорошему для консистентности стоит переименовать эти опции в noUnreachableCode и noUnusedLabels и отправить в соответствующую группу. Остальные флаги: noImplicitUseStrict, noStrictGenericChecks, suppressExcessPropertyErrors и suppressImplicitAnyIndexErrors можно было бы вынести в новую группу и назвать её Base Strict Checks. И вот почему: Эти флаги по умолчанию false и должны быть false для строгости! Вот мы и нашли те опции, которые включены по умолчанию и составляют типобезопасность TypeScript из коробки! По сути, вышеперечисленные опции (кроме noImplicitUseStrict) включают явные нарушения типизации, что в некоторых случаях можно использовать как временное решение при портировании сложных участков кода с JavaScript. Ни один флаг из данной четвёрки не придётся переопределять в 99.9% случаев. Теперь буквально по паре слов об этой псевдо-группе опций. Рекомендую: всегда для production / сложность: легко / правило ESLint: no-unreachable, пока не реализовано Флаг запрещает недосягаемый код – код, написанный после операторов return, throw, break, continue: Данный флаг будет удобно выключать для development. Аналогично правилу линтера no-unreachable, которое не реализовано для TypeScript. Рекомендую: для production если не используется ESLint / сложность: легко / правило ESLint: no-unused-labels Опция запрещает неиспользуемые лэйблы. Не думаю, что можно встретить данную синтаксическую конструкцию в наши дни, но тем не менее: Заменим правилом линтера no-unused-labels. Настроен по умолчанию, изменять противопоказано / связан с alwaysStrict Флаг автоматически добавляет "use strict" если target версия меньше, чем ES6. Опция alwaysStrict в свою очередь делает тоже самое, но для любых target. Вряд ли когда-нибудь возникнет необходимость переопределять значение данного флага. Если же обоим флагам noImplicitUseStrict и alwaysStrict установить значение true, то возникнет ошибка компиляции, так как настройки противоречат друг другу. Настроен по умолчанию, изменять противопоказано Флаг проверяет, чтобы объект не мог содержать свойства, которые не были описаны в его структуре: Можно временно выключать данную настройку при миграции кодовой базы с JavaScript, если в коде много подобных моментов. Но после нужно обязательно восстановить исходное значение. Для разовых инцидентов лучше использовать // @ts-ignore. Настроен по умолчанию, изменять противопоказано / связан с noImplicitAny Флаг проверяет, что используя скобочную нотацию невозможно обращаться к свойствам объекта, которые в нём не объявлены ни явно, ни через произвольные параметры. Вспомним пример флага noUncheckedIndexedAccess и удалим из него произвольные параметры: Казалось бы, другого поведения здесь и быть не может. Но как я и говорил, последняя четвёрка флагов порой разрешает включить нарушение типизации для каких-то крайних случаев. Например, данный флаг часто инвертируют, когда не знают, как расширять встроенные в TypeScript тайпинги или тайпинги внешних библиотек. Это плохая практика и в таких случаях правильно использовать declaration merging. Рассмотрим пример. Мы подключили некую библиотеку, допустим Google Maps, через тэг script. Попробуем создать объект Pin: Конечно, здесь будет ошибка, так как встроенные тайпинги ничего не знают о свойстве google. Использовать // @ts-ignore проблематично, так как его придётся писать чуть ли на каждой строчке кода. Поэтому часто можно встретить такое решение – ставим suppressImplicitAnyIndexErrors: true (разрешить допущение) и пишем код следующим образом: Это имеет место быть в проектах на промежуточной стадии портирования. Но я бы не рекомендовал прибегать к подобной практике даже в этом случае. Подобная проблема решается использованием объединения деклараций: Данная практика применима и к внешним библиотекам, например для расширения req и res объектов фреймворка Express. Настроен по умолчанию, изменять противопоказано Переопределение данного флага может сделать компилятор «более лояльным» при работе с generics: Подведём итоги Advanced Секция состоит из большого количества не связанных между собой флагов. Часть из них относится к строгости компилятора. Эти флаги можно перераспределить в тематические секции Флаги allowUnreachableCode и allowUnusedLabels относятся к линтингу, их можно было бы перенести в Linter Checks Опции noImplicitUseStrict, noStrictGenericChecks, suppressExcessPropertyErrors и suppressImplicitAnyIndexErrors те самые, которые включены в TypeScript по умолчанию и составляют типобезопасность из коробки. Про остальные четыре флага можно забыть Флаг noImplicitUseStrict является вариацией флага alwaysStrict. Опции noStrictGenericChecks, suppressExcessPropertyErrors и suppressImplicitAnyIndexErrors выключают некоторые базовые проверки синтаксиса, с целью облегчения портирования сложных участков кода с JavaScript на TypeScript. В качестве заключения я бы хотел написать шпаргалку со всеми упомянутыми флагами, которые обеспечивают максимальную строгость и наглядность: Файл tsconfig-checks.json: Почему tsconfig-checks.json? В первой части статьи мы обсуждали, что для того, чтобы не смешивать настройки строгости с другими опциями, мы прибегнем к наследованию конфигов. Так и поступим. Файл tsconfig.json: А для удобства разработки, чтобы нам недостижимый код и неиспользуемые переменные не ломали компиляцию, можно сделать следующим образом. Файл tsconfig-dev.json: На этом всё. В будущем планирую разобраться с опциями оптимизации и производительности и обязательно поделиться с вами. До новых встреч! Статья основана на моём треде в коллективном аккаунте @jsunderhood. P.S: В своём аккаунте @barinbritva тоже иногда пишу про TypeScript и разработку. JavaScript Developer