Чеклист для tsconfig.json
- вторник, 18 февраля 2025 г. в 00:00:06
В этой статье я расскажу о настройках TypeScript, определяемых в файле tsconfig.json
, которых я использую в своих проектах.
В этой статье описывается в основном настройка проектов, в которых все локальные модули являются ESM. Мы почти не будем говорить об импорте CommonJS.
Также мы не будем говорить о следующем:
composite
и др. См.:Для отображения выводимых типов в исходном коде я использую пакет npm ts-expect, например:
// Проверяем, что выводимым типом `someVariable` является `boolean`
expectType<boolean>(someVariable);
Я часто использую завершающие запятые (trailing commas) в моем JSON, поскольку это поддерживается tsconfig.json
и облегчает перестановку, копирование полей и т.д.
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
.
{
"include": ["src/**/*", "test/**/*"],
}
Мы должны сообщить TS, где находятся исходные файлы. Доступные настройки:
files
— исчерпывающий список (массив) всех исходных файловinclude
позволяет определять исходные файлы с помощью массива шаблонов с подстановочными знаками (wildcards), которые интерпретируются относительно tsconfig.json
exclude
позволяет исключать файлы из набора include
с помощью массива шаблонов"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
явно.
"compilerOptions": {
"sourceMap": true,
}
sourceMap
генерирует карту исходников, связывающую транспилированный JS с оригинальным TS. Это помогает с отладкой и обычно является хорошей идеей.
.d.ts
(например, для библиотек)Если мы хотим, чтобы код TS потреблял (consume) наш транспилированный TS, нужно включить генерацию файлов .d.ts
:
"compilerOptions": {
"declaration": true,
"declarationMap": true, // позволяет импортерам переходить к исходникам
}
Опционально, можно включить исходный код TS в пакет npm и активировать declarationMap
. Это позволит импортеру, например, кликнуть по типу и перейти к определению значения, и его редактор отправит ему оригинальный исходный код.
declarationDir
По умолчанию каждый файл .d.ts
размещается рядом с файлом .js
. Для того, чтобы это изменить, можно использовать настройку declarationDir
.
"compilerOptions": {
"newLine": "lf",
"removeComments": false,
}
Приведенные значения являются дефолтными.
newLine
— определяет символы конца строки для генерируемых файлов. Допустимы следующие значения:lf
— \n
(Unix)crlf
— \r\n
(Windows)removeComments
— определяет необходимость удалять комментарии из исходного кода"compilerOptions": {
"target": "ES2024",
// Убрать, если предполагается использование DOM
"lib": [ "ES2024" ],
}
target
target
определяет, какой новый синтаксис JS транспилируется в старый синтаксис. Например, если target
имеет значение ES5
, то стрелочная функция () => {}
будет транспилирована в функциональное выражение function() {}
.
ESNext
означает "самую новую версию, поддерживаемую установленным TS". Поскольку эти версии меняются между версиями TS, это может привести к проблемам при обновленииИнтересно, должна ли быть настройка для отключения транспиляции? С другой стороны, возможность писать современный JS для старых браузеров является очень удобной.
Мы должны выбирать версию ECMAScript, которая работает на всех целевых платформах. Для этого можно воспользоваться одной из следующих таблиц:
Все официальные базы также содержат target
.
lib
lib
определяет, какие доступны типы встроенных API, например, Math
или методы встроенных типов:
ES2024
и DOM
, и подкатегории, такие как DOM.Iterable
и ES2024.Promise
lib
могут записываться любым способомКогда TS поддерживает определенный API? Этот API должен быть "доступен без префиксов/флагов хотя бы в 2 разных браузерных движках (не только в 2 браузерах на основе Chromium)" (источник).
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')) {}
Типы для Node.js содержатся в отдельном пакете:
npm i -D @types/node
module
Следующие настройки определяют, как TS ищет импортируемые модули:
"compilerOptions": {
"module": "Node16",
"noUncheckedSideEffectImports": true,
}
module
С помощью этой настройки определяется система обработки модулей. При правильном значении этой настройки можно забыть про настройку moduleResolution
, которая получит хорошее дефолтное значение. Документация TS рекомендует устанавливать одно из 2 значений:
Node16
поддерживает как CommonJS, так и последние возможности ESMmoduleResolution
устанавливается в значение 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'
ничего не устанавливает.
noUncheckedSideEffectImports
По умолчанию, TS не жалуется на импорт несуществующего файла. Это объясняется тем, что некоторые сборщики ассоциируют артефакты, не являющиеся TS, с модулями. Поэтому TS интересуют только файлы TS. Пример импорта не TS:
import './component-styles.css';
Это приводит к тому, что TS не жалуется на импорт несуществующего файла TS/JS. Ошибка возникнет только при попытке что-либо импортировать из такого файла:
import './does-not-exist.js'; // ошибки нет!
Установка noUncheckedSideEffectImports
в значение true
меняет это. Позже мы поговорим об альтернативном импорте прочих (не TS) артефактов.
В настоящее время большинство небраузерных платформ умеют выполнять код TS напрямую, без необходимости его транспиляции:
"compilerOptions": {
"allowImportingTsExtensions": true,
// Требуется только при компиляции в JS
"rewriteRelativeImportExtensions": true,
}
allowImportingTsExtensions
— позволяет при импорте ссылаться на TS версию модуля, а не на его транспилированную версиюrewriteRelativeImportExtensions
— позволяет транспилировать код TS, предназначенный для прямого выполнения. По умолчанию, TS не меняет спецификаторы модулей при импорте. Здесь есть несколько нюансов:baseUrl
и paths
exports
и imports
, не считаются относительными и не перезаписываются"compilerOptions": {
"resolveJsonModule": true,
}
Эта настройка позволяет импортировать файлы JSON как модули:
import config from './config.json' with { type: 'json' };
console.log(config.hello);
При импорте файла basename.ext
с расширением, которое незнакомо TS, он заглядывает в файл basename.d.ts
. Если ext
там отсутствует, вызывается ошибка. Документация TS содержит хороший пример такого файла.
Существует 2 способа избежать проблем с импортом незнакомых файлов.
Во-первых, можно использовать настройку allowArbitraryExtensions
для подавления всех ошибок такого типа.
Во-вторых, можно создать объявление модуля окружения (ambient module declaration) с подстановочным знаком — файл .d.ts
. Следующий пример подавляет все ошибки, связанные с импортом файлов с расширением .css
:
// ./src/globals.d.ts
declare module "*.css" {}
"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
exactOptionalPropertyTypes
Если true
, то в следующем примере .colorTheme
может быть опущено, но не установлено в undefined
:
interface Settings {
// Отсутствие свойства означает 'system'
colorTheme?: 'dark' | 'light';
}
const obj1: Settings = {}; // разрешено
const obj2: Settings = { colorTheme: undefined }; // запрещено
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; // запрещено
noUncheckedIndexedAccess
Если true
, то типом неизвестного свойства будет объединение (union) undefined
и типа сигнатуры индекса:
interface ObjectWithId {
id: string,
[key: string]: string;
}
declare const obj: ObjectWithId;
expectType<string>(obj.id);
expectType<undefined | string>(obj.unknownProp);
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]
По умолчанию, следующие настройки генерируют предупреждения в редакторах, но мы может выбрать генерацию ошибок компиляции или игнорирование соответствующих проблем:
allowUnreachableCode
allowUnusedLabels
noUnusedLocals
noUnusedParameters
"compilerOptions": {
"verbatimModuleSyntax": true,
"isolatedDeclarations": true,
}
Компилятор TS выполняет 3 задачи:
В настоящее время внешние инструменты могут выполнять последние две задачи намного быстрее. Для этого требуется следующее:
Существует 2 настройки для установки этих ограничений — они вызывают ошибки компиляции, но не влияют на генерацию JS и определений:
verbatimModuleSyntax
помогает с компиляцией TS в JSisolatedDeclarations
помогает с компиляцией TS в определения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 {}
isolatedModules
Активация verbatimModuleSyntax
также активирует isolatedModules
, которая защищает нас от использования некоторых других возможностей, которые могут приводить к проблемам.
Кроме того, эта настройка позволяет esbuild компилировать TS в JS (источник).
isolatedDeclarations
isolatedDeclarations
заставляет нас добавлять аннотации типов к тому, что возвращают экспортируемые функции и методы. Это означает, что внешним инструментам не нужно выводить типы возвращаемых значений самостоятельно.
Информация о релизе TS 5.5 содержит подробный раздел об изолированных определениях.
noEmit
Иногда мы хотим использовать TS только для проверки типов, например, когда мы запускаем TS вручную или используем внешние инструменты для компиляции файлов TS (в файлы JS, определения и т.д.):
"compilerOptions": {
"noEmit": true,
}
noEmit
— если true
, tsc
будет только проверять типы, он не будет генерировать никакие файлыУдалять ли настройки генерации файлов зависит от того, используются ли они внешними инструментами.
Проблемы импорта модуля CommonJS из модуля ESM:
.default
объекта пространства имен модуляmodule.exports
функциейСуществует 2 настройки, которые могут с этим помочь.
allowSyntheticDefaultImports
Эта настройка влияет только на проверку типов, она не влияет на генерацию файлов JS: если true
, дефолтный импорт модуля CommonJS указывает на module.exports
(а не на module.exports.default
), но только при отсутствии module.exports.default
.
Это имитирует то, как Node.js обрабатывает дефолтные импорты модулей CommonJS (источник): "При импорте модулей CommonJS объект module.exports
предоставляется как дефолтный экспорт. Именованные экспорты могут быть доступны как результат статического анализа для обеспечения лучшей совместимости с экосистемой".
Нужна ли нам эта настройка? Да, но она автоматически включается при moduleResolution: 'bundler'
или module: 'Node16'
(которая активирует esModuleInterop
, которая активирует allowSyntheticDefaultImports
).
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
.__esModule
, то он всегда импортируется без учета esModuleInterop
Нужна ли нам эта настройка? Нет, если мы работаем только с модулями ESM.
Обычно, мы не трогаем следующие настройки:
moduleDetection
— эта настройка влияет на то, как TS определяет, является файл скриптом или модулем. Дефолтное значение auto
хорошо работает в большинстве случаев. Значение force
требуется только в случае, когда в кодовой базе есть модуль, который не содержит ни импортов, ни экспортов. Если module
имеет значение Node16
и package.json
содержит "type": "module"
, то такие файлы будут автоматически считаться модулямиskipLibCheck
— до тех пор, пока вы не трогаете файлы определений типов библиотек, эту настройку лучше не активировать (у ее активации много недостатков)package.json
TS учитывает несколько свойств package.json
:
type
— это важная настройка. Если код компилируется в модули ESM, то package.json
должен содержать:"type": "module"
exports
определяет, какие файлы является публично доступными, и переопределяет пути (пути, которые видит импортер, будут отличаться от внутренних путей). Эти настройки могут применяться условно, в зависимости от среды выполнения (браузер, Node.js и т.д.). Более подробную информацию можно найти в этой статьеimports
позволяет определять синонимы, такие как #util
для внешних модулей и пакетов. Более подробную информацию об этом свойстве можно найти здесьЕсли вы недовольны спецификаторами модулей для локальных импортов в автоматически создаваемых импортах, можете взглянуть на следующие настройки VSCode:
javascript.preferences.importModuleSpecifierEnding
typescript.preferences.importModuleSpecifierEnding
В настоящее время VSCode достаточно умный, чтобы добавлять расширения файлов при необходимости.
Результатом моих хождений по мукам является такой 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,
}
}
"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 объясняет, с чем это связано.
"compilerOptions": {
// ···
//===== Другое =====
"lib": ["ES2024"], // не предоставляет типы для DOM
}
module: 'Node16'
должна хорошо работать для сборщиков. Но можно переключиться на module: 'Preserve'
, предназначенный специально для сборщиков.
"compilerOptions": {
"allowImportingTsExtensions": true,
// Требуется только при компиляции в JS
"rewriteRelativeImportExtensions": true,
}
"compilerOptions": {
"noEmit": true,
}
tsconfig.json
от других авторовНовости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩