TypeScript: Раскладываем tsconfig по полочкам. Часть 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
.
Завершение первой части
В этой статье я расписал самые важные флаги и опции, которые могут понадобиться в подавляющем большинстве проектов. В следующей же части я расскажу про настройку строгости компилятора. Ссылку добавлю после выхода статьи.