
В этой статье я расскажу о настройках 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. Настройки для проверки типов имеют хорошие значения по умолчанию
По умолчанию, следующие настройки генерируют предупреждения в редакторах, но мы может выбрать генерацию ошибок компиляции или игнорирование соответствующих проблем:
allowUnreachableCodeallowUnusedLabelsnoUnusedLocalsnoUnusedParameters
❯ 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.importModuleSpecifierEndingtypescript.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?