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

?v=1

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

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

Если открыть официальный референс tsconfig, то там будет полный список всех настроек, разделённых по группам. Однако, это не даёт понимания, с чего начать и что из данного обширного списка опций обязательно, а на что можно не обращать внимания (по крайней мере до поры до времени). Плюс, иногда опции сгруппированы по некому техническому, а не логическому смыслу. Например, некоторые флаги проверок можно найти в группе Strict Checks, некоторые в Linter Checks, а другие в Advanced. Это не всегда удобно для понимания.

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

Структура tsconfig

Рассмотрим структуру и некоторые особенности конфига.

  • tsconfig.json состоит из двух частей. Какие-то опции необходимо указывать в root, а какие-то в compilerOptions

  • tsconfig.json поддерживает комментарии. Такие IDE как WebStorm и Visual Studio Code знают об этом не выделяют комментарии как синтаксическую ошибку

  • tsconfig.json поддерживает наследование. Опции можно разделить по некоторому принципу, описать их в разных файлах и объединить с помощью специальной директивы

Это болванка нашего tsconfig.json:

{
  // extends позволяет обогатить опции другими опциями из указанного файла
  // файлом tsconfig-checks.json займёмся во второй части статьи
  "extends": "./tsconfig-checks.json",
  // в корне конфига находятся project-specific опции
  "compilerOptions": {
    // здесь все настройки, связанные с компилятором
  }
}

К root опциям относится только следующие: extends, files, include, exclude, references, typeAcquisition. Из них мы будем рассматривать первые 4. Все остальные опции размещаются в compilerOptions.

Иногда в root секции конфига можно встретить такие опции как compileOnSave и ts-node. Эти опции не являются официальными и используются IDE для своих целей.

Секция root

extends

Type: string | false, default: false.

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

{
  "compilerOptions": {
    // блок базовых настроек

    // блок настроек строгости
  }
}

Рассмотрим другой use-case, где комментариями отделаться не получится. Если необходимо создать production и development конфиги. Так бы мог выглядеть tsconfig-dev.json версия конфига:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    // переопределяем настройки, которые нужны только для dev режима
    "sourceMap": true,
    "watch": true
  }
}

В целом, я рекомендую пользоваться extends. Однако, сильно дробить настройки не рекомендую. Это может привести к запутыванию. В том числе по причине того, что множественное наследование не поддерживается.

Если вы решите использовать эту опцию. То увидеть итоговую, объединённую версию конфига поможет команда tsc --showConfig.

files

Type: string[] | false, default: false, связана с include.

Указать список конкретных файлов для компиляции можно использовав данную опцию.

{
  "compilerOptions": {},
  "files": [
    "core.ts",
    "app.ts"
  ]
}

Данная опция подходит лишь для совсем маленьких проектов из нескольких файлов.

include

Type string[], default: зависит от значения files, связана с exclude.

Если опция files не указана, то TypeScript будет использовать эту директиву для поиска компилируемых файлов. Если include так же не указана, то её значение будет неявно объявлено как ["**/*"]. Это означает, что поиск файлов будет осуществляться во всех папках и их подпапках. Такое поведение не оптимально, поэтому в целях производительности лучше всегда указывать конкретные пути. Можно прописывать как пути к конкретным файлам, так и паттерны путей.

{
  "compilerOptions": {},
  "include": [
    "src/**/*",
    "tests/**/*"
  ]
}

Если паттерны не указывают конкретных расширений, то TypeScript будет искать файлы с расширениями .ts, .tsx, and .d.ts. А если включен флаг allowJs, то ещё .js и .jsx.

Следующие форматы записей делают одно и тоже src, ./src, src/**/*. Я предпочитаю вариант ./src.

Технически, используя опции include и exclude, TypeScript сгенерирует список всех подходящих файлов и поместит их в files. Это можно наблюдать если выполнить команду tsc --showConfig.

exclude

Type: string[], default: [«nodemodules», «bowercomponents», «jspm_packages»].

Директива служит для того, чтобы исключать некоторые лишние пути или файлы, которые включились директивой include. По умолчанию, опция имеет значение путей пакетных менеджеров npm, bower и jspm, так как модули в них уже собраны. Помимо этого, TypeScript будет так же игнорировать папку из опции outDir, если она указана. Это папка, куда помещаются собранные артефакты сборки. Логично, что их нужно исключить. Если добавить свои значения в эту опцию, то необходимо не забыть восстановить умолчания. Так как пользовательские значения не объединяются со значениями по умолчанию. Другими словами, необходимо вручную указать корень модулей своего пакетного менеджера.

{
  "compilerOptions": {},
  "exclude": [
    "node_modules",
    "./src/**/*.spec.ts"
  ]
}

Опция exclude не может исключить файлы, указанные через files.

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

Секция compilerOptions

target

Type: string, default: ES3, влияет на опции lib, module.

Версия стандарта ECMAScript, в которую будет скомпилирован код. Здесь большой выбор: ES3, ES5, ES6 (он же ES2015), ES2016, ES2017, ES2018, ES2019, ES2020, ESNext. Для backend приложений/пакетов подойдёт ES6, если рассчитываете только на современные версии Node.js и ES5, если хотите поддержать более старые версии. На данный момент стандарт ES6, с небольшими оговорками, поддерживается 97.29% браузеров. Так что для frontend приложений ситуация аналогичная.

module

Type: string, default: зависит от target, влияет на опцию moduleResolution.

Модульная система, которую будет использовать ваше собранное приложение. На выбор: None, CommonJS, AMD, System, UMD, ES6, ES2015, ES2020 or ESNext. Для backend приложений/пакетов подойдёт ES6 или CommonJS в зависимости от версий Node.js, которые хотите поддерживать. Для frontend приложений под современные браузеры также подходит ES6. А для поддержки более старых браузеров и для изоморфных приложений, определённо стоит выбрать UMD.

Если ваша ситуация не такая простая или хотите знать все тонкости модульных систем, тогда придётся всё-таки изучить подробную документацию.

moduleResolution

Type: string, default: зависит от module.

Стратегия, которая будет использоваться для импорта модулей. Здесь всего две опции: node и classic. При этом classic в 99% не будет использоваться, так как это legacy. Однако, я специально упомянул этот флаг, так как он меняется в зависимости от предыдущего флага. При изменении значения module режим moduleResolution может переключиться на classic и в консоли начнут появляться сообщения об ошибках на строчках с импортами.

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

lib

Type: string[], default: зависит от target.

В зависимости от того какой target установлен в конфиге, TypeScript подключает тайпинги (*.d.ts-файлы) для поддержки соответствующих спецификаций. Например, если ваш target установлен в ES6, то TypeScript подключит поддержку array.find и прочих вещей, которые есть в стандарте. Но если target стоит ES5, то использовать метод массива find нельзя, так как его не существует в этой версии JavaScript. Можно подключить полифилы. Однако, для того, чтобы TypeScript понял, что теперь данную функциональность можно использовать, необходимо подключить необходимые тайпинги в секции lib. При этом, можно подключить как весь стандарт ES2015, так и его часть ES2015.Core (только методы find, findInex и т.д.).

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

Для --target ES5 подключаются: DOM, ES5, ScriptHost
Для --target ES6: DOM, ES6, DOM.Iterable, ScriptHost

Как только вы что-либо добавляете в lib умолчания сбрасываются. Необходимо руками добавить то, что нужно, например DOM:

{
  "compilerOptions": {
    "target": "ES5",
    "lib": [
      "DOM",
      "ES2015.Core"
    ]
  }
}

outDir

Type: string, default: равняется корневой директории.

Конечная папка, куда будут помещаться собранные артефакты. К ним относятся: .js, .d.ts, и .js.map файлы. Если оставить не указывать значение для данной опции, то все вышеуказанные файлы будут повторять структуру исходных файлов в корне вашего проекта. В таком случае будет сложно удалять предыдущие билды и описывать .gitignore файлы. Да и кодовая база будет похожа на свалку. Советую складывать все артефакты в одну папку, которую легко удалить или заигнорировать системой контроля версий.

Если оставить опцию outDir пустой:

├── module
│   └── core.js
│   └── core.ts
├── index.js
└── index.ts

Если указать outDir:

├── dist
│   └── module
│   |   └── core.js
│   └── index.js
├── module
│   └── core.ts
└── index.ts

outFile

Type: string, default: none.

Судя по описанию, данная опция позволяет объединить все файлы в один. Кажется, что бандлеры вроде webpack больше не нужны… Однако, опция работает только если значение module указано None, System, or AMD. К огромному сожалению, опция не будет работать с модулями CommonJS or ES6. Поэтому скорее всего использовать outFile не придётся. Так как опция выглядит максимально привлекательно, но работает не так как ожидается, я решил предупредить вас об этом гигантском подводном камне.

allowSyntheticDefaultImports

Type: boolean, default: зависит от module или esModuleInterop.

Если какая-либо библиотека не имеет default import, лоадеры вроде ts-loader или babel-loader автоматически создают их. Однако, d.ts-файлы библиотеки об этом не знают. Данный флаг говорит компилятору, что можно писать следующим образом:

// вместо такого импорта
import * as React from 'react';
// можно писать такой
import React from 'react';

Опция включена по умолчанию, если включен флаг esModuleInterop или module === «system».

esModuleInterop

Type: boolean, default: false.

За счёт добавления болерплейта в выходной код, позволяет импортировать CommonJS пакеты как ES6.

// библиотека moment экспортируется только как CommonJS
// пытаемся импортировать её как ES6
import Moment from 'moment';

// без флага esModuleInterop результат undefined
console.log(Moment);

// c флагом результат [object Object]
console.log(Moment);

Данный флаг по зависимости активирует allowSyntheticDefaultImports. Вместе они помогают избавиться от зоопарка разных импортов и писать их единообразно по всему проекту.

alwaysStrict

Type: boolean, default: зависит от strict.

Компилятор будет парсить код в strict mode и добавлять "use strict” в выходные файлы.

По умолчанию false, но если включен флаг strict, то true.

downlevelIteration

Type: boolean, default: false.

Спецификация ES6 добавила новый синтаксис для итерирования: цикл for / of, array spread, arguments spread. Если код проекта преобразовывается в ES5, то конструкция с циклом for / of будет преобразована в обычный for:

// код es6
const str = 'Hello!';
for (const s of str) {
  console.log(s);
}
// код es5 без downlevelIteration
var str = "Hello!";
for (var _i = 0, str_1 = str; _i < str_1.length; _i++) {
  var s = str_1[_i];
  console.log(s);
}

Однако, некоторые символы, такие как emoji кодируются с помощью двух символов. Т. е. такое преобразование в некоторых местах будет работать не так, как ожидается. Включенный флаг downlevelIteration генерирует более многословный и более «правильный», но менее производительный код. Код получается действительно очень большим, поэтому не буду занимать место на экране. Если интересно посмотреть пример, то перейдите в playground и выберете в настройках target -> es5, downlevelIteration -> true.

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

forceConsistentCasingInFileNames

Type: boolean, default: false.

Включает режим чувствительности к регистру (case-sensitive) для импорта файлов. Таким образом, даже в case-insensitive файловых системах при попытке сделать импорт import FileManager from './FileManager.ts', если файл в действительности называется fileManager.ts, приведёт к ошибке. Перестраховаться лишний раз не повредит. TypeScript — это про строгость.

Опции секции compilerOptions, которые нужны не в каждом проекте

declaration

Type: boolean, default: false.

С помощью включения данного флага, помимо JavaScript файлов, к ним будут генерироваться файлы-аннотации, известные как d.ts-файлы или тайпинги. Благодаря тайпингам становится возможным определение типов для уже скомпилированных js файлов. Другими словами код попадает в js, а типы в d.ts-файлы. Это полезно в случае, например, если вы публикуете свой пакет в npm. Такой библиотекой смогут пользоваться разработчики, которые пишут как на чистом JavaScript, так и на TypeScript.

declarationDir

Type: string, default: none, связан с declaration.

По умолчанию тайпинги генерируются рядом с js-файлами. Используя данную опцию можно перенаправить все d.ts-файлы в отдельную папку.

emitDeclarationOnly

Type: boolean, default: false, связан с declaration.

Если по какой-то причине вам нужны только d.ts-файлы, то включение данного флага предотвратит генерацию js-файлов.

allowJs

Type: boolean, default: false.

Портировать ваш JavaScript проект на TypeScript поможет данный флаг. Активировав allowJs TypeScript компилятор будет обрабатывать не только ts файлы, но и js. Нет нужды полностью мигрировать проект, прежде чем продолжить его разработку. Можно это делать файл за файлом, просто меня расширение и добавляя типизацию. А новый функционал сразу можно писать на TypeScript.

checkJs

Type: boolean, default: false, связан с allowJs.

TypeScript будет проверять ошибки не только в ts, но и в js-файлах. Помимо встроенных тайпингов для языковых конструкций JavaScript, TS-компилятор так же умеет использовать jsDoc для анализа файлов. Я предпочитаю не использовать этот флаг, а наводить порядок в коде в момент его типизации. Однако, если в вашем проекте хорошее покрытие кода jsDoc, стоит попробовать.

С версии 4.1 при включении checkJs, флаг allowJs включается автоматически.

experimentalDecorators и emitDecoratorMetadata

Type: boolean, default: false.

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

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

Для работы emitDecoratorMetadata необходимо подтянуть в проект библиотеку reflect-metadata.

resolveJsonModule

Type: boolean, default: false.

Флаг позволяет включить возможность импортировать *.json файлы. Ничего дополнительно устанавливать не требуется.

// необходимо указывать расширение .json
import config from './config.json'

jsx

Type: string, default: none.

Если проект использует React, необходимо включить поддержку jsx. В подавляющем большинстве случаев будет достаточно опций react или react-native. Так же есть возможность оставить jsx-код нетронутым с помощью опции preserve или использовать кастомные преобразователи react-jsx и react-jsx.

Завершение первой части

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

© Habrahabr.ru