
В этой статье я расскажу о настройках TypeScript, определяемых в файле tsconfig.json
, которых я использую в своих проектах.
❯ 1. Возможности, не затрагиваемые в этой статье
В этой статье описывается в основном настройка проектов, в которых все локальные модули являются ESM. Мы почти не будем говорить об импорте CommonJS.
Также мы не будем говорить о следующем:
- импорт и проверка типов обычного JavaScript — настройках allowJs и checkJs
- настройка JSX. См. раздел "JSX" карманной книги по TS
- "проекты" (полезно для монорепозиториев): настройка
composite
и др. См.:
❯ 2. Заметки
Для отображения выводимых типов в исходном коде я использую пакет npm ts-expect, например:
// Проверяем, что выводимым типом `someVariable` является `boolean`
expectType<boolean>(someVariable);
Я часто использую завершающие запятые (trailing commas) в моем JSON, поскольку это поддерживается tsconfig.json
и облегчает перестановку, копирование полей и т.д.
❯ 3. Расширение базовых файлов с помощью extends
Эта настройка позволяет нам ссылаться на существующий tsconfig.json
с помощью спецификатора модуля (module specifier) (как если бы мы импортировали файл JSON). Этот файл становится основой/базой (base) для расширения (extend) нашим tsconfig.json
. Это означает, что наш tsconfig.json
содержит все настройки базового, может перезаписывать их и добавлять новые.
Репозиторий GitHub tsconfig/bases содержит все базы, доступные в пространстве имен @tsconfig
, которые могут использоваться следующим образом (после локальной установки с помощью npm):
{
"extends": "@tsconfig/node-lts/tsconfig.json",
}
Ни один из этих файлов мне не подходит. Но они могут послужить хорошей основой для вашего tsconfig.json
.
❯ 4. Исходные файлы
{
"include": ["src/**/*", "test/**/*"],
}
Мы должны сообщить TS, где находятся исходные файлы. Доступные настройки:
-
files
— исчерпывающий список (массив) всех исходных файлов -
include
позволяет определять исходные файлы с помощью массива шаблонов с подстановочными знаками (wildcards), которые интерпретируются относительноtsconfig.json
-
exclude
позволяет исключать файлы из набораinclude
с помощью массива шаблонов
❯ 5. Готовые файлы
5.1. Директория для записи готовых файлов
"compilerOptions": {
"rootDir": ".",
"outDir": "dist",
}
Вот как TS определяет, куда записывать готовые файлы:
- он берет файл по указанному пути (относительно
tsconfig.json
) - удаляет префикс, определенный с помощью
rootDir
и - помещает результат в
outDir
Дефолтным значением rootDir
является самый длинный общий префикс относительных путей исходных файлов.
В качестве примера рассмотрим следующий tsconfig.json
:
{
"include": ["src/**/*", "test/**/*"],
"compilerOptions": {
"rootDir": ".",
"outDir": "dist",
}
}
Структура проекта:
/tmp/my-proj/
tsconfig.json
src/
main.ts
test/
test.ts
Результат работы компилятора TS:
/tmp/my-proj/
dist/
src/
main.js
test/
test.js
Если мы удалим rootDir
из tsconfig.json
, результат будет таким же, поскольку дефолтным значением этой настройки является "."
.
Однако, результат будет другим, если мы изменим include
:
{
"include": ["src/**/*"],
"compilerOptions": {
"outDir": "dist",
}
}
Теперь дефолтным значением rootDir
будет src
и результат будет таким:
/tmp/my-proj/
dist/
main.js
Поскольку дефолтное значение rootDir
зависит от include
, я предпочитаю определять rootDir
явно.
5.2. Генерация карт исходников
"compilerOptions": {
"sourceMap": true,
}
sourceMap
генерирует карту исходников, связывающую транспилированный JS с оригинальным TS. Это помогает с отладкой и обычно является хорошей идеей.
5.3. Генерация файлов .d.ts
(например, для библиотек)
Если мы хотим, чтобы код TS потреблял (consume) наш транспилированный TS, нужно включить генерацию файлов .d.ts
:
"compilerOptions": {
"declaration": true,
"declarationMap": true, // позволяет импортерам переходить к исходникам
}
Опционально, можно включить исходный код TS в пакет npm и активировать declarationMap
. Это позволит импортеру, например, кликнуть по типу и перейти к определению значения, и его редактор отправит ему оригинальный исходный код.
5.3.1. Настройка declarationDir
По умолчанию каждый файл .d.ts
размещается рядом с файлом .js
. Для того, чтобы это изменить, можно использовать настройку declarationDir
.
5.4. Тонкая настройка генерируемых файлов
"compilerOptions": {
"newLine": "lf",
"removeComments": false,
}
Приведенные значения являются дефолтными.
-
newLine
— определяет символы конца строки для генерируемых файлов. Допустимы следующие значения:
-
lf
—\n
(Unix) -
crlf
—\r\n
(Windows)
-
-
removeComments
— определяет необходимость удалять комментарии из исходного кода
❯ 6. Возможности языка и платформы
"compilerOptions": {
"target": "ES2024",
// Убрать, если предполагается использование DOM
"lib": [ "ES2024" ],
}
6.1. target
target
определяет, какой новый синтаксис JS транспилируется в старый синтаксис. Например, если target
имеет значение ES5
, то стрелочная функция () => {}
будет транспилирована в функциональное выражение function() {}
.
- tsconfig/bases содержит рекомендуемые настройки для разных платформ
- значение
ESNext
означает "самую новую версию, поддерживаемую установленным TS". Поскольку эти версии меняются между версиями TS, это может привести к проблемам при обновлении
Интересно, должна ли быть настройка для отключения транспиляции? С другой стороны, возможность писать современный JS для старых браузеров является очень удобной.
6.1.1. Выбор правильной цели
Мы должны выбирать версию ECMAScript, которая работает на всех целевых платформах. Для этого можно воспользоваться одной из следующих таблиц:
- для браузеров — compat-table.github.io
- для Node.js — node.green
Все официальные базы также содержат target
.
6.2. lib
lib
определяет, какие доступны типы встроенных API, например, Math
или методы встроенных типов:
- документация TS описывает, как значения могут добавляться в массив. Полный их список можно найти в репозитории TS
- существуют категории, такие как
ES2024
иDOM
, и подкатегории, такие какDOM.Iterable
иES2024.Promise
- значения не регистрозависимы: автодополнения VSCode содержат много заглавных букв, названия файлов не содержат. Значения
lib
могут записываться любым способом
Когда TS поддерживает определенный API? Этот API должен быть "доступен без префиксов/флагов хотя бы в 2 разных браузерных движках (не только в 2 браузерах на основе Chromium)" (источник).
6.2.1. Настройка lib
через target
target
определяет дефолтное значение lib
: если последнее опущено, а значением target
является ES2024
, тогда значением lib
будет ES2024.Full
. Однако, сами ES2024.Full
мы использовать не можем. Если мы хотим сделать это вручную, то должны перечислить в lib
все, что содержится в es2024.full.d.ts
:
/// <reference lib="es2024" />
/// <reference lib="dom" />
/// <reference lib="webworker.importscripts" />
/// <reference lib="scripthost" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
Мы можем наблюдать интересный феномен в этом файле:
- категория
ES20YY
обычно включает все ее подкатегории - категория
DOM
не включает, например,DOM.Iterable
пока не является ее частью
Среди прочего, DOM.Iterable
позволяет перебирать список узлов (NodeList):
for (const $div of document.querySelectorAll('div')) {}
6.3. Типы встроенных API Node.js
Типы для Node.js содержатся в отдельном пакете:
npm i -D @types/node
❯ 7. Модульная система
7.1. module
Следующие настройки определяют, как TS ищет импортируемые модули:
"compilerOptions": {
"module": "Node16",
"noUncheckedSideEffectImports": true,
}
7.1.1. module
С помощью этой настройки определяется система обработки модулей. При правильном значении этой настройки можно забыть про настройку moduleResolution
, которая получит хорошее дефолтное значение. Документация TS рекомендует устанавливать одно из 2 значений:
- Node.js:
Node16
поддерживает как CommonJS, так и последние возможности ESM
-
moduleResolution
устанавливается в значениеNode16
- существует также значение
NodeNext
, но оно является динамическим. В настоящее время оно эквивалентноNode16
, но может измениться в будущем, что может сломать код
-
- сборщики:
preserve
поддерживает как CommonJS, так и последние возможности ESM. Оно совпадает с тем, что делает большинство сборщиков
-
moduleResolution
устанавливается в значениеbundler
-
Таким образом, большинство сборщиков подражает Node.js. Я всегда использую Node16
и не сталкивался ни с какими проблемами.
Обратите внимание, что в обоих случаях TS заставляет нас указывать полные названия импортируемых локальных модулей. Мы не может опускать расширения файлов, как было принято во времена, когда Node.js компилировался только в CommonJS. Новый подход отражает то, как работают ESM.
module: 'Node16'
устанавливает target: 'es2022'
, но я предпочитаю устанавливать target
вручную, поскольку module
и target
связаны не так тесно, как module
и moduleResolution
. Кроме того, module: 'bundler'
ничего не устанавливает.
7.1.2. noUncheckedSideEffectImports
По умолчанию, TS не жалуется на импорт несуществующего файла. Это объясняется тем, что некоторые сборщики ассоциируют артефакты, не являющиеся TS, с модулями. Поэтому TS интересуют только файлы TS. Пример импорта не TS:
import './component-styles.css';
Это приводит к тому, что TS не жалуется на импорт несуществующего файла TS/JS. Ошибка возникнет только при попытке что-либо импортировать из такого файла:
import './does-not-exist.js'; // ошибки нет!
Установка noUncheckedSideEffectImports
в значение true
меняет это. Позже мы поговорим об альтернативном импорте прочих (не TS) артефактов.
7.2. Отключение генерации файлов
В настоящее время большинство небраузерных платформ умеют выполнять код TS напрямую, без необходимости его транспиляции:
"compilerOptions": {
"allowImportingTsExtensions": true,
// Требуется только при компиляции в JS
"rewriteRelativeImportExtensions": true,
}
-
allowImportingTsExtensions
— позволяет при импорте ссылаться на TS версию модуля, а не на его транспилированную версию -
rewriteRelativeImportExtensions
— позволяет транспилировать код TS, предназначенный для прямого выполнения. По умолчанию, TS не меняет спецификаторы модулей при импорте. Здесь есть несколько нюансов:
- перезаписываются только относительные пути
- они перезаписываются "наивно", без учета настроек
baseUrl
иpaths
- пути, определяемые через
exports
иimports
, не считаются относительными и не перезаписываются
7.3. Импорт JSON
"compilerOptions": {
"resolveJsonModule": true,
}
Эта настройка позволяет импортировать файлы JSON как модули:
import config from './config.json' with { type: 'json' };
console.log(config.hello);
7.4. Импорт прочих артефактов
При импорте файла basename.ext
с расширением, которое незнакомо TS, он заглядывает в файл basename.d.ts
. Если ext
там отсутствует, вызывается ошибка. Документация TS содержит хороший пример такого файла.
Существует 2 способа избежать проблем с импортом незнакомых файлов.
Во-первых, можно использовать настройку allowArbitraryExtensions
для подавления всех ошибок такого типа.
Во-вторых, можно создать объявление модуля окружения (ambient module declaration) с подстановочным знаком — файл .d.ts
. Следующий пример подавляет все ошибки, связанные с импортом файлов с расширением .css
:
// ./src/globals.d.ts
declare module "*.css" {}
❯ 8. Проверка типов
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
}
strict
, на мой взгляд, является обязательной настройкой. Что касается остальных настроек, решайте сами, насколько строгим должен быть ваш код. Можно начать с добавления всех настроек, и смотреть, какие из них будут слишком проблематичными, на ваш вкус. В этом разделе мы не будем говорить о настройках, охватываемых strict
(таких как noImplicitAny
).
-
noFallthroughCasesInSwitch
— еслиtrue
, непустые блокиcase
в инструкцииswitch
должны заканчиватьсяbreak
,return
илиthrow
-
noImplicitOverride
— еслиtrue
, методы, перезаписывающие методы суперкласса, должны иметь модификаторoverride
-
noImplicitReturns
— еслиtrue
, тогда "неявный возврат" (конец функции или метода) разрешается только в случае, когда возвращаемым типом являетсяvoid
8.1. exactOptionalPropertyTypes
Если true
, то в следующем примере .colorTheme
может быть опущено, но не установлено в undefined
:
interface Settings {
// Отсутствие свойства означает 'system'
colorTheme?: 'dark' | 'light';
}
const obj1: Settings = {}; // разрешено
const obj2: Settings = { colorTheme: undefined }; // запрещено
8.2. noPropertyAccessFromIndexSignature
Если true
, то для типов, таких как в следующем примере, нельзя использовать точечную нотацию для доступа к неизвестным свойствам:
interface ObjectWithId {
id: string,
[key: string]: string;
}
declare const obj: ObjectWithId;
const value1 = obj.id; // разрешено
const value2 = obj['unknownProp']; // разрешено
const value3 = obj.unknownProp; // запрещено
8.3. noUncheckedIndexedAccess
Если true
, то типом неизвестного свойства будет объединение (union) undefined
и типа сигнатуры индекса:
interface ObjectWithId {
id: string,
[key: string]: string;
}
declare const obj: ObjectWithId;
expectType<string>(obj.id);
expectType<undefined | string>(obj.unknownProp);
8.3.1. noUncheckedIndexedAccess
и массивы
Настройка noUncheckedIndexedAccess
также влияет на обработку массивов:
const arr = ['a', 'b'];
const elem = arr[0];
expectType<undefined | string>(elem);
Если эта настройка имеет значение false
, типом элемента массива будет string
.
Распространенной практикой является проверка длины массива перед доступом к элементу. Однако, это не работает с noUncheckedIndexedAccess
:
function logElemAt0(arr: Array<string>) {
if (0 < arr.length) {
const elem = arr[0];
expectType<undefined | string>(elem);
console.log(elem);
}
}
В данном случае следует использовать другой подход:
function logElemAt0(arr: Array<string>) {
if (0 in arr) {
const elem = arr[0];
expectType<string>(elem);
console.log(elem);
}
}
С одной стороны, новый подход отражает тот факт, что массивы могут содержать дыры. С другой стороны, начиная с ES6, JS считает дыры элементами со значением undefined
:
> Array.from([,,,])
[undefined, undefined, undefined]
8.4. Настройки для проверки типов имеют хорошие значения по умолчанию
По умолчанию, следующие настройки генерируют предупреждения в редакторах, но мы может выбрать генерацию ошибок компиляции или игнорирование соответствующих проблем:
allowUnreachableCode
allowUnusedLabels
noUnusedLocals
noUnusedParameters
❯ 9. Совместимость: облегчение внешним инструментам компиляции TS в JS и определений типов
"compilerOptions": {
"verbatimModuleSyntax": true,
"isolatedDeclarations": true,
}
Компилятор TS выполняет 3 задачи:
- Проверка типов.
- Генерация файлов JS.
- Генерация файлов определений (declaration files).
В настоящее время внешние инструменты могут выполнять последние две задачи намного быстрее. Для этого требуется следующее:
- генерация результата не должна требовать поиска информации в файлах, импортируемых исходным файлом
- также не нужен семантический анализ, только синтаксический
Существует 2 настройки для установки этих ограничений — они вызывают ошибки компиляции, но не влияют на генерацию JS и определений:
-
verbatimModuleSyntax
помогает с компиляцией TS в JS -
isolatedDeclarations
помогает с компиляцией TS в определения
9.1. verbatimModuleSyntax
Большую часть "типовых" частей файла TS легко определить. Исключением являются импорт: без (относительно простого) семантического анализа мы не знаем, импортируется тип (TS) или значение (JS).
Если verbatimModuleSyntax
включен, мы делаем добавление ключевого слова type
к импортам типов обязательным:
// Исходный код
import { type SomeInterface, SomeClass } from './my-module.js';
// Результат
import { SomeClass } from './my-module.js';
Обратите внимание, что класс — это и значение, и тип. В данном случае type
указывать не нужно, поскольку эта часть синтаксиса может остаться в JS.
Нам также нужно указывать type
при экспорте типов:
interface MyInterface {}
export { type MyInterface };
// Альтернатива
export interface MyInterface {}
9.1.1. isolatedModules
Активация verbatimModuleSyntax
также активирует isolatedModules
, которая защищает нас от использования некоторых других возможностей, которые могут приводить к проблемам.
Кроме того, эта настройка позволяет esbuild компилировать TS в JS (источник).
9.2. isolatedDeclarations
isolatedDeclarations
заставляет нас добавлять аннотации типов к тому, что возвращают экспортируемые функции и методы. Это означает, что внешним инструментам не нужно выводить типы возвращаемых значений самостоятельно.
Информация о релизе TS 5.5 содержит подробный раздел об изолированных определениях.
9.3. noEmit
Иногда мы хотим использовать TS только для проверки типов, например, когда мы запускаем TS вручную или используем внешние инструменты для компиляции файлов TS (в файлы JS, определения и т.д.):
"compilerOptions": {
"noEmit": true,
}
-
noEmit
— еслиtrue
,tsc
будет только проверять типы, он не будет генерировать никакие файлы
Удалять ли настройки генерации файлов зависит от того, используются ли они внешними инструментами.
❯ 10. Импорт CommonJS из ESM
Проблемы импорта модуля CommonJS из модуля ESM:
- в ESM дефолтный экспорт — это свойство
.default
объекта пространства имен модуля - в CommonJS объект модуля — это дефолтный экспорт, например, существует много модулей CommonJS, которые делают
module.exports
функцией
Существует 2 настройки, которые могут с этим помочь.
10.1. allowSyntheticDefaultImports
Эта настройка влияет только на проверку типов, она не влияет на генерацию файлов JS: если true
, дефолтный импорт модуля CommonJS указывает на module.exports
(а не на module.exports.default
), но только при отсутствии module.exports.default
.
Это имитирует то, как Node.js обрабатывает дефолтные импорты модулей CommonJS (источник): "При импорте модулей CommonJS объект module.exports
предоставляется как дефолтный экспорт. Именованные экспорты могут быть доступны как результат статического анализа для обеспечения лучшей совместимости с экосистемой".
Нужна ли нам эта настройка? Да, но она автоматически включается при moduleResolution: 'bundler'
или module: 'Node16'
(которая активирует esModuleInterop
, которая активирует allowSyntheticDefaultImports
).
10.2. esModuleInterop
Эта настройка влияет на компиляцию кода CommonJS:
- если
false
:
-
import * as m from 'm'
компилируется вconst m = require('m')
-
import m from 'm'
компилируется (грубо) вconst m = require('m')
, а каждый доступ кm
компилируется в доступ кm.default
-
- если
true
:
-
import * as m from 'm'
добавляет новый объект кm
, который содержит те же свойства, чтоmodule.exports
, и свойство.default
, указывающее наmodule.exports
-
import m from 'm'
добавляет новый объект кm
с единственный свойством.default
, указывающим наmodule.exports
. Каждый доступ кm
компилируется в доступ кm.default
-
- если модуль CommonJS имеет свойство
.__esModule
, то он всегда импортируется без учетаesModuleInterop
Нужна ли нам эта настройка? Нет, если мы работаем только с модулями ESM.
❯ 11. Другие настройки с хорошими значениями по умолчанию
Обычно, мы не трогаем следующие настройки:
-
moduleDetection
— эта настройка влияет на то, как TS определяет, является файл скриптом или модулем. Дефолтное значениеauto
хорошо работает в большинстве случаев. Значениеforce
требуется только в случае, когда в кодовой базе есть модуль, который не содержит ни импортов, ни экспортов. Еслиmodule
имеет значениеNode16
иpackage.json
содержит"type": "module"
, то такие файлы будут автоматически считаться модулями -
skipLibCheck
— до тех пор, пока вы не трогаете файлы определений типов библиотек, эту настройку лучше не активировать (у ее активации много недостатков)
❯ 12. Настройки TS в package.json
TS учитывает несколько свойств package.json
:
-
type
— это важная настройка. Если код компилируется в модули ESM, тоpackage.json
должен содержать:
"type": "module"
-
exports
определяет, какие файлы является публично доступными, и переопределяет пути (пути, которые видит импортер, будут отличаться от внутренних путей). Эти настройки могут применяться условно, в зависимости от среды выполнения (браузер, Node.js и т.д.). Более подробную информацию можно найти в этой статье -
imports
позволяет определять синонимы, такие как#util
для внешних модулей и пакетов. Более подробную информацию об этом свойстве можно найти здесь
❯ 13. VSCode
Если вы недовольны спецификаторами модулей для локальных импортов в автоматически создаваемых импортах, можете взглянуть на следующие настройки VSCode:
javascript.preferences.importModuleSpecifierEnding
typescript.preferences.importModuleSpecifierEnding
В настоящее время VSCode достаточно умный, чтобы добавлять расширения файлов при необходимости.
❯ 14. Заключение
Результатом моих хождений по мукам является такой tsconfig.json
:
{
"include": ["src/**/*", "test/**/*"],
"compilerOptions": {
// Определяем явно (не полагаемся на include):
"rootDir": ".",
"outDir": "dist",
//===== Результат: JavaScript =====
"target": "ES2024",
"module": "Node16", // устанавливает "moduleResolution"
// Разрешаем импорт пустых модулей
"noUncheckedSideEffectImports": true,
//
"sourceMap": true, // .js.map files
//===== Совместимость с внешними инструментами =====
// Помогает инструментам, компилирующим .ts в .js, делая
// модификаторы `type` обязательными для импорта типа и т.д.
"verbatimModuleSyntax": true,
//===== Проверка типов =====
"strict": true, // активирует несколько полезных настроек
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
//===== Другие настройки =====
// Разрешаем импорт файлов JSON
"resolveJsonModule": true,
}
}
14.1. Пакет npm (библиотеки и т.п.)
"compilerOptions": {
// ···
//===== Результат: определения =====
"declaration": true, // файлы .d.ts
// "Go to definition" переходит к исходнику TS source и т.д.
"declarationMap": true, // файлы .d.ts.map
//===== Совместимость с внешними инструментами =====
// Помогает инструментам, компилирующим .ts в .d.ts, делая обязательными аннотации типов
// возвращаемых экспортируемыми функциями значений и т.д.
"isolatedDeclarations": true,
//===== Другое =====
"lib": ["ES2024"], // не предоставляет типы для DOM
}
Обратите внимание: если библиотека использует DOM, lib
следует удалить.
Я всегда включаю настройку isolatedDeclarations
, но TS разрешает это только если активирована настройка declaration
или настройка composite
. Jake Bailey объясняет, с чем это связано.
14.2. Приложение Node.js
"compilerOptions": {
// ···
//===== Другое =====
"lib": ["ES2024"], // не предоставляет типы для DOM
}
14.3. Веб-приложение
module: 'Node16'
должна хорошо работать для сборщиков. Но можно переключиться на module: 'Preserve'
, предназначенный специально для сборщиков.
14.4. Прямой запуск TS без генерации файлов JS
"compilerOptions": {
"allowImportingTsExtensions": true,
// Требуется только при компиляции в JS
"rewriteRelativeImportExtensions": true,
}
14.5. Использование TS только для проверки типов
"compilerOptions": {
"noEmit": true,
}
15. tsconfig.json
от других авторов
- Matt Pocock's "The TSConfig Cheat Sheet"
- Pelle Wessman's base.json
- Sindre Sorhus' tsconfig.json
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
Комментарии (2)
Nurked
18.02.2025 05:56Всё красиво, но горький опыт показывает, что чем меньше настроек было сделано в компиляторе, тем меньше боли испытаеваешь, когда надо вернуться к проекту через 2 года.
meonsou
Как же приятно видеть в статье источники, почаще бы так писали. А почему нету в хабе Typescript?