TypeScript: Раскладываем tsconfig по полочкам. Часть 2

983c4608d7e8695c6208d15672398738.png

В прошлой статье я рассказал о различных особенностях некоторых общих настроек TypeScript. В данной статье речь пойдёт о так называемых «флагах строгости».

На самом деле, TypeScript из коробки мало чем отличается от JavaScript. Поэтому если изначально не подтюнить конфиг проекта, то большая часть преимуществ языка не будет задействована. Смысл использования TypeScript в таком виде есть, но небольшой.

Если выразить смысл всей статьи в одном предложении, то текст можно свести к следующему: откройте свой tsconfig.json, установите флаг strict в секции compilerOptions в значение true. Без режима строгости использование TypeScript это практически пустая трата времени. Однако цель предыдущей и данной статей заключается не в том, чтобы сказать как «правильно» делать, а прояснить некоторые тонкости конфигурации. Поэтому погрузимся в детали данного вопроса.

В официальном референсе tsconfig.json его многочисленные опции поделены на секции. Из них две секции: Strict Checks и Linter Checks — содержат только опции тех самых флагов строгости. Помимо ещё часть интересующих нас сегодня флагов сокрыта в самой большой группе опций Advanced.

Группа Strict Checks

Пожалуй, флаги данной категории наиболее важные из всех. Вот они: strict, alwaysStrict, noImplicitAny, strictNullChecks, strictFunctionTypes, strictPropertyInitialization, noImplicitThis, strictBindCallApply.

Все вышеперечисленные флаги по умолчанию имеют значение false, а для того, чтобы было строго нужно установить противоположное значение — true.

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

alwaysStrict

Рекомендован: всегда / сложность: легко.

Флаг alwaysStrict включает добавление строки "use strict" в каждый скомпилированный файл. Другими словами, alwaysStrict включает строгий режим JavaScript и никак не связан с проверкой типов TypeScript.

strict

Рекомендован: всегда / сложность: сложно / может быть заменён набором других флагов.

Флаг strict напрямую связан с проверкой типов. Его включение автоматически активирует абсолютно все флаги секции Strict Checks, включая и alwaysStrict. Это именно то, о чём я говорил в самом начале.

У такого подхода есть как минимум один недостаток — неочевидность. Устанавливая strict: true, нет наглядного представления, какие именно проверки включены и какие опции вообще существуют. Для проектов, которые с самого начала пишутся на TypeScript это не так принципиально, как для проектов, которые поэтапно портируются с JavaScript.

В процессе портирования существующего приложения нет возможности сразу включить все проверки. Приходится активировать их по одной шаг за шагом. Иногда даже случаются сложности, из-за которых приходится откатывать ранее установленные флаги обратно в false.

Есть небольшая особенность работы флага strict — список подконтрольных ему флагов может пополняться по мере выхода новых версий TypeScript. Подобные моменты если случаются, то редко и всегда освещаются в release notes если, конечно, вы их читаете перед обновлением версии.

Лично я предпочитаю указывать список флагов явным образом:

{
  "compilerOptions": {
    "alwaysStrict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictPropertyInitialization": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "strictBindCallApply": true,
  }
}

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

noImplicitAny

Рекомендую: всегда / сложность: средне

Это первый флаг, который необходимо активировать в начале работы над проектом с TypeScript.

Для начала несколько слов про any. Это специальный тип, который назначается всем переменным, если их тип не был задан явно и не может быть выведен компилятором автоматически. Данный тип создан для обратной совместимости с JavaScript. С точки зрения TypeScript, все переменные в JavaScript это any, поскольку в нём нет системы типов.

Для компилятора тип any означает «это может быть что угодно, передаю управление и ответственность разработчику». Так как компилятор не знает тип переменной, с такой переменной можно делать всё что угодно. В этом месте TypeScript полностью лишён своей силы:

// тип задан явно
let a: number = 5
// тип выведен
let b = 'hello'

// тип не указан и не может быть выведен
// value будет неявно объявлено как any
function someFunction (value) {
  // поэтому ошибка в этой строке останется незамеченной
  console.log(value.subtr(3))
}

Иначе говоря, если тип переменной не определён, значит он any, значит это JavaScript, а не TypeScript код. Т. е. количество неопределённых типов в коде, является реальным показателем отношения TypeScript кода к JavaScript коду. Флаг noImplicitAny подсвечивает все такие места, для того чтобы разработчик не забывал указывать типы.

Однако это не означает, что теперь вовсе нельзя использовать any. Это означает лишь, что в подобных ситуация разработчик должен явно (implicit) писать any, если по каким-то причинам не получается правильно описать тип переменной или если указание типа переменной приводит к ошибкам в другом участке кода.

// явно помечаем код как потенциально не безопасный через any
function someFunction (value: any) {
  console.log(value.subtr(3))
}

Явное присутствие any напоминает разработчику о том, что код не доработан и эти места должны быть улучшены со временем. В этом заключается суть флага noImplicitAny.

Дополнительно усилить эффект можно включив ESLint правило no-explicit-any. Правило будет отмечать места с any как warning, каждый раз привлекая внимание разработчика.

strictNullChecks

Рекомендую: всегда / сложность: средне

Флаг активирует одну из самых мощных особенностей TypeScript. Рекомендую включить его сразу после noImplicitAny.

JavaScript содержит два нижних значения — undefined и null, для которых в TypeScript есть одноимённые типы. По иерархии считается, что все остальные типы происходят от них. Таким образом по принципу наследования вместо переменной любого типа: string, boolean, number и т. д — можно передать значение undefined или null:

function someFunction (value: number) {
  // можно получить неожиданный результат если value будет undefined или null
  return value * 2
}

someFunction(5)
// по умолчанию можно так
someFunction(null)
// и можно так
someFunction(undefined)

Подобное поведение справедливо для почти всех (если не всех) языков программирования. На практике это может вызывать неудобства в виде написания дополнительных проверок на null (undefined). Но гораздо большей проблемой является помнить о необходимости покрывать код данными проверками. Думаю, Java разработчики с их NullPointerException как никто другой понимают эту проблему.

Включение опции strictNullChecks меняет правила игры. С данным флагом компилятор больше не позволяет передавать undefined или null туда, где ожидаются переменные других типов, если это не разрешено явно. Рассмотрим пример:

function someFunction (value: number) {
  // value всегда будет только number
  return value * 2
}

someFunction(5)
// следующие вызовы функции невозможны
someFunction(null)
someFunction(undefined)

Это позволяет не обрабатывать ситуации с undefined и null в тех участках кода, где возникновение данных значений невозможно. При этом в тем местах, где возникновение подобных ситуаций возможно, компилятор не позволит забыть о проверках. Это очень сильный механизм, существование которого сложно переоценить.

// Символ «?» разрешает undefined, а «| null» - null
function someFunction (value?: number | null) {
  if (value == null) {
    return 0
  }
  return value * 2
}

Есть только один потенциально опасный момент. В нашем статически типизированном коде всё будет работать как часы. Однако приложения часто работают с динамически получаемыми данными (например, данными от сервера), типы которых могут быть описаны одним образом, а на практике всё может быть совсем иначе. Другими словами, в типах описано, что некоторое значение не может быть null, а сервер его пришлёт. Избежать подобной ситуации можно введя практику проверять ответы от сервера. Например, с помощью json-схем валидаторов.

Чуть более коварной может быть ситуация с внешними библиотеками. В частности, с теми, который изначально написаны на JavaScript и тайпинги для них существуют отдельно от кода. В таких случаях потенциальные несовпадения реального кода с типами могут привести к возникновению вышеописанной ситуации.

Здесь можно найти таблицу совместимости типов с включенным и выключенным режимом strictNullChecks — Any, unknown, object, void, undefined, null and never assignability.

strictPropertyInitialization

Рекомендую: всегда / сложность: легко / связан с strictNullChecks

Флаг strictPropertyInitialization следит, чтобы объявленные свойства класса всегда были инициализированы:

class User {
  name: string
  // email не инициализирован ни здесь, ни в конструкторе
  // компилятор подскажет, что нужно установить значение
  email: string

  constructor (name: string) {
    this.name = name
  }
}

Опция является своего рода дополнением к флагу strictNullChecks поэтому работает только когда второй флаг тоже включен.

strictFunctionTypes

Рекомендую: всегда / сложность: легко

Установка флага strictFunctionTypes: true включает более строгую проверку сигнатур функций. В целом, данная опция выглядит достаточно обязательной для использования:

interface StringOrNumberFunc {
  (value: string | number): void
}

function someFunction (value: string) {
  console.log(value)
}

// сигнатуры не совпадают
// string | number не эквивалентны string
let func: StringOrNumberFunc = someFunction

func(10)
func('10')

noImplicitThis

Рекомендую: всегда / сложность: легко

При использовании this проверяет, что контекст выполнения известен. Рассмотрим такой пример:

class SomeClass {
  multiplier: number = 5

  createSomeFunction (value: number) {
    return function () {
      // контекст потерян - здесь this НЕ является объектом класса SomeClass
      return value * this.multiplier
    }
  }
}

Объект this не известен, так как function не пробрасывает контекст автоматически. В данном случае это можно исправить, заменив function на arrow function.

Рассмотрим пример независимой функции, которая по каким-то причинам должна использовать внешний this:

function sayHello (name: string) {
  console.log(this.helloWord + ' ' + name)
}

В данном случае this также неизвестен. Выполнить функцию с внешним this можно с помощью bind, call, apply. А научить функцию понимать контекст можно следующим образом:

// укажем тип this первым «аргументом»
// фактически это не будет считаться аргументом функции
function sayHello (this: HelloStyle, name: string) {
  console.log(this.helloWord + ' ' + name)
}

// объявим варианты приветствий
interface HelloStyle {
  helloWord: string
}

class HawaiiStyle implements HelloStyle {
  helloWord = 'Aloha'
}

class RussianStyle implements HelloStyle {
  helloWord = 'Привет,'
}

// теперь вызовем
sayHello.bind(new HawaiiStyle())('World')
sayHello.call(new RussianStyle(), 'World')
sayHello.apply(new RussianStyle(), ['World'])

Во всех случаях тип контекста известен и даже тайпхинтинг работает как следует.

strictBindCallApply

Рекомендую: всегда / сложность: средне

Флаг strictBindCallApply — включает более строгую проверку сигнатур при использовании соответствующих методов: bind, call, apply.

function someFunction (value: string) {
  console.log(value)
}

someFunction.call(undefined, '10')
// да, для таких случаев нужен отдельный флаг
someFunction.call(undefined, false)

Кроме некоторых специфичных ситуаций при работе с данным флагом проблем не возникает. В случае возникновения сложных ситуаций рекомендую выключить проверку в месте возникновения проблемы через комментарий // @ts-ignore вместо полного отключения правила.

Подведение итогов Strict Checks

  • здесь действительно все флаги очень полезные. С ними TypeScript раскрывает свой потенциал и приносит максимум пользы

  • все флаги можно включить разом через strict: true или каждый по отдельности. Второй вариант отлично подходит для смешанных кодовых баз, где часть кода ещё не переведена на TypeScript.

  • alwaysStrict не про строгий режим TypeScript, а про строгий режим JavaScript. Т. е. про "use strict".

  • флаги первоочерёдной важности это noImplicitAny и strictNullChecks в сочетании с strictPropertyInitialization. Затем идут strictFunctionTypes и noImplicitThis. А напоследок strictBindCallApply.

Группа Linter Checks

Название группы говорит само за себя — подразумевается, что флаги этой группы проверяют не соответствие типов, а качество кода. Своего рода правила как в ESLint. И действительно некоторые правила секции Linter Checks можно заменить на аналогичные правила ESLint. Полный список флагов данной секции:

{
  "compilerOptions": {
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,
    
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
  }
}

Все вышеперечисленные флаги по умолчанию имеют значение false, а для того, чтобы было строго нужно установить противоположное значение — true.

Я неспроста отделил флаги noPropertyAccessFromIndexSignature и noUncheckedIndexedAccess пустой строкой от других флагов группы. Дело в том, что они находятся здесь только по причине того, что концептуально не могут располагаться в группе Strict Checks. По задумке в этой секции находятся флаги, которые подконтрольны опции strict.

Но эти флаги слишком специфичные для того, чтобы включать их по умолчанию. Практически так и сказано в анонсах noPropertyAccessFromIndexSignature и noUncheckedIndexedAccess. Однако это не делает их правилами линтинга, они проверяют типы и по факту относятся к Strict Checks, только не включаются автоматически при включении флага strict.

Остальные же флаги группы полноправно находятся в этой секции и имеют аналогичные правила в ESLint. В некоторых случаях с временными ограничениями, так как ещё не все правила перенесены в ESLint из TSLint. Соответствия правил можно посмотреть на странице TSLint Migration Guide и в репозитории ESLint rules for TSLint.

Рассмотрим работу каждого из флагов.

noPropertyAccessFromIndexSignature

Рекомендую: всегда / сложность: легко / есть нюанс / нельзя заменить линтером

Флаг noPropertyAccessFromIndexSignature запрещает обращаться к свойствам объекта через точку aka dot notation, если свойства объекта описаны не явно, а через произвольные параметры (aka arbitrarily-named properties, index signatures).

interface User {
  // явно указанные параметры
  login: string
  email: string

  // произвольные параметры
  [key: string]: string
}

const user: User = {
  login: 'hello',
  email: 'hello@example.com'
}

// c noPropertyAccessFromIndexSignature: true
// данная строка приведёт к ошибке
const username = user.name

// но так обращаться всё ещё можно
const username2 = user['name']

Вместо точечной нотации мы используем скобочную нотацию aka bracket notation. Идея флага примерно такая же как у noImplicitAny — привлечь внимание к потенциально проблемным местам.

Единственной причиной не использовать данную опцию — наличие двух видов синтаксиса для обращения к свойствам объекта. Однако на мой взгляд в этом нет проблемы так как хоть синтаксис и разный, у каждого из них есть своё назначение. Всегда используем dot notation и, если случается исключение, переходим на скобочную нотацию. При этом код становится безопаснее. Поэтому я рекомендую использовать этот флаг.

На самом деле есть ещё одна причина, но она решается флагом noUncheckedIndexedAccess.

noUncheckedIndexedAccess

Рекомендую: по ситуации / сложность: средне / есть серьёзный нюанс / связан с strictNullChecks / нельзя заменить линтером

Ещё раз взглянем на предыдущий пример.

interface User {
  // явно указанные параметры
  login: string
  email: string

  // произвольные параметры
  [key: string]: string
}

const user: User = {
  login: 'hello',
  email: 'hello@example.com'
}

// Тип переменной username - string
const username = user['name']

Как мы видим, тип переменной username является string. И это не совсем правильно так как мы можем обратиться к любому свойству, даже если его не существует. Можно описывать произвольные поля следующим образом: [key: string]: string | undefined. Это решит проблему, но добавит ручной работы. Плюс, из-за человеческого фактора можно периодически забывать дописывать undefined.

Флаг noUncheckedIndexedAccess сделает эту работу за нас. Теперь компилятор не позволит использовать полученные таким образом переменные без предварительной проверки.

Рассмотрим ещё один пример. На этот раз с массивом:

const strings: string[] = ['hello']
// здесь переменная number будет иметь тип string
const number = strings[100]

Однако очевидно, что в данном случае переменная будет undefined. С включенным noUncheckedIndexedAccess в подобных ситуациях тип переменной будет определяться как string | undefined.

Казалось бы, мы подчеркнули скобочной нотацией «магические» свойства объекта, мы можем использовать их только после проверок на undefined — всё красиво и безопасно. Что может пойти не так?

Если кратко:

const strings: string[] = ['hello']
// в данном случае переменная number также будет иметь тип string | undefined
const number = strings[0]

Или другой пример:

function upperCaseAll(strings: string[]) {
  for (let i = 0; i < strings.length; i++) {
      // здесь будет ошибка компиляции
      console.log(strings[i].toUpperCase());
  }
}

Другими словами, при данной конфигурации в обмен на безопасность в некоторых случаях придётся платить проверками, которые не несут смысла только для того, чтобы укротить строптивый компилятор. Выглядит это крайне костыльно.

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

Флаг является своего рода дополнением к флагу strictNullChecks поэтому работает только когда второй флаг тоже включен.

noImplicitReturns

Рекомендую: всегда / сложность: легко / правило ESLint: consistent-return, пока не реализовано

Флаг проверяет, чтобы все ветки функции возвращали значение:

function lookupHeadphonesManufacturer(color: 'blue' | 'black'): string {
  if (color === 'blue') {
    return 'beats'
  }
  // забыли return
}

Аналогично правилу ESLint consistent-return, которое в данный момент ещё не реализовано для TypeScript.

noFallthroughCasesInSwitch

Рекомендую: если не используется ESLint / сложность: легко / правило ESLint: no-fallthrough

Флаг проверяет наличие break в операторе switch/case:

switch (value) {
  case 0:
    console.log('even')
    // забыли break
  case 1:
    console.log('odd')
    break
}

Заменяется правилом no-fallthrough.

noUnusedLocals

Рекомендую: для production если не используется ESLint / сложность: легко / правило ESLint: no-unused-vars

Код проверяется на наличие неиспользуемых переменных:

function createKeyboard (modelID: number) {
  // объявлена неиспользуемая переменная
  const defaultModelID = 23

  return {
    type: 'keyboard',
    modelID
  }
}

Данное правило крайне неудобно при использовании в процессе разработки. В процессе написания кода он может находится в «разобранном» состоянии, а наличие неиспользуемых переменных будет вызывать ошибку компиляции.

Можно переопределять флаг для development окружения или использовать правило ESLint no-unused-vars.

noUnusedParameters

Рекомендую: для production если не используется ESLint / сложность: легко / правило ESLint: no-unused-vars

Код проверяется на наличие неиспользуемых аргументов функций и методов:

function createDefaultKeyboard (modelID: number) {
  const defaultModelID = 23

  return {
    type: 'keyboard',
    modelID: defaultModelID
  }
}

Данное правило также как noUnusedParameters мешает удобному процессу разработки. Можно переопределять флаг для development окружения или добавить дополнительные параметры к проверке ESLint no-unused-vars.

Подведение итогов Linter Checks

  • нет единого флага, которым можно активировать все опции секции

  • опции noPropertyAccessFromIndexSignature и noUncheckedIndexedAccess по факту относятся к Strict Checks, только не активируются автоматически при включении главного флага strict

  • остальные флаги секции: noFallthroughCasesInSwitch, noUnusedLocals, noUnusedParameters можно заменить правилами линтера. Только правило для noImplicitReturns временно не поддерживается

Группа Advanced

Это самая большая секция и кажется, что в неё поместили все опции, которым не нашлось отдельных групп. Из них часть опций относится к настройкам строгости компилятора. Вот они:

{
  "compilerOptions": {
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    
    "noImplicitUseStrict": false,
    "suppressExcessPropertyErrors": false,
    "suppressImplicitAnyIndexErrors": false,
    "noStrictGenericChecks": false,
  }
}

Первые два флага allowUnreachableCode и allowUnusedLabels опять отделены пустой строкой не случайно — они отбились от Linter Checks.

Внезапно по умолчанию эти флаги имеют значение true, однако для строгости должны быть false — ровно наоборот.

Запомнить это поведение достаточно легко. У предыдущих опций такого рода в названиях фигурировал префикс no, т. е. «запретить допущения». У данных же флагов префикс allow — «разрешить допущения». По-хорошему для консистентности стоит переименовать эти опции в noUnreachableCode и noUnusedLabels и отправить в соответствующую группу.

Остальные флаги: noImplicitUseStrict, noStrictGenericChecks, suppressExcessPropertyErrors и suppressImplicitAnyIndexErrors можно было бы вынести в новую группу и назвать её Base Strict Checks. И вот почему:

Эти флаги по умолчанию false и должны быть false для строгости!

Вот мы и нашли те опции, которые включены по умолчанию и составляют типобезопасность TypeScript из коробки!

По сути, вышеперечисленные опции (кроме noImplicitUseStrict) включают явные нарушения типизации, что в некоторых случаях можно использовать как временное решение при портировании сложных участков кода с JavaScript.

Ни один флаг из данной четвёрки не придётся переопределять в 99.9% случаев.

Теперь буквально по паре слов об этой псевдо-группе опций.

allowUnreachableCode

Рекомендую: всегда для production / сложность: легко / правило ESLint: no-unreachable, пока не реализовано

Флаг запрещает недосягаемый код — код, написанный после операторов return, throw, break, continue:

function fn (n: number) {
  if (n > 5) {
    return true
  } else {
    return false
  }

  // недосягаемый код
  return true
}

Данный флаг будет удобно выключать для development. Аналогично правилу линтера no-unreachable, которое не реализовано для TypeScript.

allowUnusedLabels

Рекомендую: для production если не используется ESLint / сложность: легко / правило ESLint: no-unused-labels

Опция запрещает неиспользуемые лэйблы. Не думаю, что можно встретить данную синтаксическую конструкцию в наши дни, но тем не менее:

function verifyAge (age: number) {
  if (age > 18) {
    // тот самый неиспользуемый label
    verified: true
  }
}

Заменим правилом линтера no-unused-labels.

noImplicitUseStrict

Настроен по умолчанию, изменять противопоказано / связан с alwaysStrict

Флаг автоматически добавляет "use strict" если target версия меньше, чем ES6. Опция alwaysStrict в свою очередь делает тоже самое, но для любых target. Вряд ли когда-нибудь возникнет необходимость переопределять значение данного флага.

Если же обоим флагам noImplicitUseStrict и alwaysStrict установить значение true, то возникнет ошибка компиляции, так как настройки противоречат друг другу.

suppressExcessPropertyErrors

Настроен по умолчанию, изменять противопоказано

Флаг проверяет, чтобы объект не мог содержать свойства, которые не были описаны в его структуре:

interface Point {
  x: number
  y: number
}

const p: Point = {
  x: 1,
  y: 3,
  // свойство z не объявлено в интерфейсе Point
  z: 10
}

Можно временно выключать данную настройку при миграции кодовой базы с JavaScript, если в коде много подобных моментов. Но после нужно обязательно восстановить исходное значение. Для разовых инцидентов лучше использовать // @ts-ignore.

suppressImplicitAnyIndexErrors

Настроен по умолчанию, изменять противопоказано / связан с noImplicitAny

Флаг проверяет, что используя скобочную нотацию невозможно обращаться к свойствам объекта, которые в нём не объявлены ни явно, ни через произвольные параметры. Вспомним пример флага noUncheckedIndexedAccess и удалим из него произвольные параметры:

interface User {
  // явно указанные параметры
  login: string
  email: string

  // закомментируем произвольные параметры
  // [key: string]: string
}

const user: User = {
  login: 'hello',
  email: 'hello@example.com'
}

// теперь обратиться к name нельзя
const username = user['name']

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

Рассмотрим пример. Мы подключили некую библиотеку, допустим Google Maps, через тэг script. Попробуем создать объект Pin:

const pin = new window.google.maps.Pin(59.9386, 30.3141)

Конечно, здесь будет ошибка, так как встроенные тайпинги ничего не знают о свойстве google. Использовать // @ts-ignore проблематично, так как его придётся писать чуть ли на каждой строчке кода. Поэтому часто можно встретить такое решение — ставим suppressImplicitAnyIndexErrors: true (разрешить допущение) и пишем код следующим образом:

const pin = new window['google']['maps']['Pin'](59.9386, 30.3141)

Это имеет место быть в проектах на промежуточной стадии портирования. Но я бы не рекомендовал прибегать к подобной практике даже в этом случае. Подобная проблема решается использованием объединения деклараций:

// произвольный файл merging.d.ts
interface Pin {
  // описание
}

interface PinConstructor {
  new(lat: number, lng: number): Pin
}

interface Window {
  google: {
    maps: {
      Pin: PinConstructor
    }
  }
}

// использование в коде
const pin = new window.google.maps.Pin(59.9386, 30.3141)

Данная практика применима и к внешним библиотекам, например для расширения req и res объектов фреймворка Express.

noStrictGenericChecks

Настроен по умолчанию, изменять противопоказано

Переопределение данного флага может сделать компилятор «более лояльным» при работе с generics:

type A = (x: T, y: U) => [T, U]
type B = (x: S, y: S) => [S, S]

function f (a: A, b: B) {
  // OK
  b = a
  // должна быть ошибка, но компилятор проигнорирует
  a = b
}

Подведём итоги Advanced

  • Секция состоит из большого количества не связанных между собой флагов. Часть из них относится к строгости компилятора. Эти флаги можно перераспределить в тематические секции

  • Флаги allowUnreachableCode и allowUnusedLabels относятся к линтингу, их можно было бы перенести в Linter Checks

  • Опции noImplicitUseStrict, noStrictGenericChecks, suppressExcessPropertyErrors и suppressImplicitAnyIndexErrors те самые, которые включены в TypeScript по умолчанию и составляют типобезопасность из коробки.

  • Про остальные четыре флага можно забыть

  • Флаг noImplicitUseStrict является вариацией флага alwaysStrict.

  • Опции noStrictGenericChecks, suppressExcessPropertyErrors и suppressImplicitAnyIndexErrors выключают некоторые базовые проверки синтаксиса, с целью облегчения портирования сложных участков кода с JavaScript на TypeScript.

Заключение

В качестве заключения я бы хотел написать шпаргалку со всеми упомянутыми флагами, которые обеспечивают максимальную строгость и наглядность:

Файл tsconfig-checks.json:

{
  "compilerOptions": {
    // Strict Checks
    "alwaysStrict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictPropertyInitialization": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "strictBindCallApply": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,
    // Linter Checks
    "noImplicitReturns": true, // https://eslint.org/docs/rules/consistent-return ?
    "noFallthroughCasesInSwitch": true, // https://eslint.org/docs/rules/no-fallthrough
    "noUnusedLocals": true, // https://eslint.org/docs/rules/no-unused-vars
    "noUnusedParameters": true, // https://eslint.org/docs/rules/no-unused-vars#args
    "allowUnreachableCode": false, // https://eslint.org/docs/rules/no-unreachable ?
    "allowUnusedLabels": false, // https://eslint.org/docs/rules/no-unused-labels
    // Base Strict Checks
    "noImplicitUseStrict": false,
    "suppressExcessPropertyErrors": false,
    "suppressImplicitAnyIndexErrors": false,
    "noStrictGenericChecks": false,
  }
}

Почему tsconfig-checks.json? В первой части статьи мы обсуждали, что для того, чтобы не смешивать настройки строгости с другими опциями, мы прибегнем к наследованию конфигов. Так и поступим.

Файл tsconfig.json:

{
  // настройки строгости
  "extends": "./tsconfig-checks.json",
  "compilerOptions": {
    // все остальные настройки
  }
}

А для удобства разработки, чтобы нам недостижимый код и неиспользуемые переменные не ломали компиляцию, можно сделать следующим образом.

Файл tsconfig-dev.json:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    // выключим некоторый линтинг, чтобы не ломалась компиляция
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "allowUnreachableCode": true,
    "allowUnusedLabels": true
  }
}

На этом всё. В будущем планирую разобраться с опциями оптимизации и производительности и обязательно поделиться с вами. До новых встреч!

Статья основана на моём треде в коллективном аккаунте @jsunderhood.

P.S: В своём аккаунте @barinbritva тоже иногда пишу про TypeScript и разработку.

© Habrahabr.ru