Перейти к содержимому

41. tsconfig.json детально

Привет, кодеры! 👋 Яша снова на связи, и сегодня мы погрузимся в сердце каждого серьезного TypeScript-проекта — файл tsconfig.json. Если package.json — это паспорт вашего проекта, то tsconfig.json — это его мозг, конституция и GPS-навигатор в одном флаконе. Он говорит компилятору TypeScript, как именно он должен понимать ваш код, какие правила применять и куда складывать результат.

Для чего он так важен? Возьмем аналогию: вы — опытный шеф-повар, а TypeScript — ваш су-шеф. tsconfig.json — это ваша поваренная книга и список ингредиентов. Без него су-шеф не знает, что готовить, какую температуру использовать и куда подавать блюдо. В продвинутых проектах, особенно с монорепозиториями или сложными структурами, правильно настроенный tsconfig.json критически важен для производительности, корректности типов и поддержания порядка.

Самая большая и важная секция в tsconfig.json — это compilerOptions. Здесь мы указываем компилятору все его инструкции.

// tsconfig.json (пример базовой конфигурации)
{
"compilerOptions": {
// 1. target: Какую версию ECMAScript мы хотим получить на выходе
// Современные приложения часто используют ES2020 или ESNext
"target": "ES2022",
// 2. module: Какую систему модулей использовать для сгенерированного JS
// CommonJS для Node.js, ESNext для бандлеров типа Webpack/Vite
"module": "ESNext",
// 3. lib: Какие стандартные библиотеки доступны в глобальной области видимости
// Включает API браузера, DOM, ES-функции.
// 'DOM' для браузерных проектов, 'ESNext' для всех новейших фич.
"lib": ["ES2022", "DOM", "DOM.Iterable"],
// 4. outDir: Куда компилировать выходные JS-файлы
"outDir": "./dist",
// 5. rootDir: Где искать исходные TS-файлы
"rootDir": "./src",
// 6. strict: Включить все "строгие" проверки типов
// Настоятельно рекомендуется для всех проектов! Он включает strictNullChecks,
// noImplicitAny, strictFunctionTypes и другие.
"strict": true,
// 7. esModuleInterop: Позволяет использовать синтаксис импорта CommonJS-модулей
// как ES-модулей (import * as React from 'react' -> import React from 'react')
"esModuleInterop": true,
// 8. skipLibCheck: Пропускать проверку типов файлов деклараций из node_modules
// Ускоряет компиляцию, но может скрыть проблемы в библиотеках.
"skipLibCheck": true,
// 9. forceConsistentCasingInFileNames: Гарантирует, что имена файлов
// всегда используются с согласованным регистром (для предотвращения проблем на разных ОС).
"forceConsistentCasingInFileNames": true,
// 10. declaration: Генерировать .d.ts файлы деклараций типов
// Необходимо при создании библиотек, чтобы потребители могли получить типы.
"declaration": true,
// 11. declarationMap: Генерировать sourcemap для .d.ts файлов
// Полезно для отладки типов в сторонних инструментах.
"declarationMap": true,
// 12. jsx: Как компилятор должен обрабатывать JSX
// "react-jsx" для нового JSX-трансформатора React.
"jsx": "react-jsx",
// 13. moduleResolution: Как компилятор разрешает модули.
// "Node" для Node.js, "NodeNext" для более строгих правил Node.js.
"moduleResolution": "Node",
// 14. baseUrl: Базовый путь для разрешения не относительных модулей
// Используется с "paths" для создания алиасов.
"baseUrl": "./src",
// 15. paths: Карта для разрешения модулей. Идеально для абсолютных импортов.
"paths": {
"@utils/*": ["./utils/*"],
"@components/*": ["./components/*"]
}
},
// 16. include: Массив globs (шаблонов), которые указывают, какие файлы
// должны быть включены в проект для компиляции.
"include": ["src/**/*.ts", "src/**/*.tsx"],
// 17. exclude: Массив globs, которые следует исключить
// (даже если они попали под "include").
"exclude": ["node_modules", "dist"]
}

Для больших проектов или монорепозиториев абсолютные импорты — это спасение. baseUrl и paths позволяют нам настроить собственные алиасы, делая код чище и удобнее для рефакторинга.

Представьте, что у вас есть такая структура:

src/
├── components/
│ └── Button.tsx
├── utils/
│ └── helpers.ts
└── index.ts

Без paths вам пришлось бы писать:

src/index.ts
import { Button } from './components/Button';
import { sum } from './utils/helpers';

С baseUrl и paths (как в примере выше):

src/index.ts
import { Button } from '@components/Button'; // Красиво, не правда ли?
import { sum } from '@utils/helpers';

Но помните: если вы используете paths, вам также нужно настроить ваш бандлер (Webpack, Vite, Rollup) или среду выполнения (Node.js с module-alias или tsconfig-paths), чтобы он тоже понимал эти алиасы. TypeScript видит их, но рантайм по умолчанию — нет!

Один из самых мощных инструментов для управления tsconfig.json в монорепозиториях или крупных проектах — это extends. Он позволяет одной конфигурации наследовать поля из другой, а затем переопределять или дополнять их.

Предположим, у вас есть общая “базовая” конфигурация для всех проектов:

base-tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"moduleResolution": "Node",
"baseUrl": "./src",
"paths": {
"@utils/*": ["./utils/*"]
}
}
}

А теперь у вас есть два проекта: frontend (с React) и backend (Node.js).

apps/frontend/tsconfig.json
{
"extends": "../../base-tsconfig.json", // Путь к базовому файлу
"compilerOptions": {
"jsx": "react-jsx", // Добавляем специфичную для React настройку
"outDir": "./dist"
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
apps/backend/tsconfig.json
{
"extends": "../../base-tsconfig.json",
"compilerOptions": {
"lib": ["ES2022"], // Переопределяем lib, DOM не нужен для бэкенда
"outDir": "./build"
},
"include": ["src/**/*.ts"]
}

Файлы base-tsconfig.json обычно не включают include/exclude, чтобы каждый проект мог определять свои собственные границы.

Для монорепозиториев references — это убийственная фича! Она позволяет TypeScript понять взаимозависимости между подпроектами, что дает несколько ключевых преимуществ:

  1. Быстрая инкрементальная сборка: TypeScript компилирует только те проекты и их зависимости, которые изменились.
  2. Улучшенная навигация и автодополнение: IDE лучше понимает связи между проектами.
  3. Типобезопасность на границах проектов: Гарантирует, что изменения в одной библиотеке корректно отражаются в ее потребителях.

Пример структуры монорепо:

my-monorepo/
├── packages/
│ ├── ui-kit/
│ │ ├── src/
│ │ │ └── Button.ts
│ │ └── tsconfig.json
│ └── utils/
│ ├── src/
│ │ └── math.ts
│ └── tsconfig.json
└── apps/
└── web/
├── src/
│ └── index.ts
└── tsconfig.json
packages/utils/tsconfig.json
{
"compilerOptions": {
"composite": true, // Обязательно для всех референсных проектов
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
// ... другие compilerOptions
},
"include": ["src"],
"exclude": ["node_modules"]
}
packages/ui-kit/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
// ... другие compilerOptions
},
"include": ["src"],
"exclude": ["node_modules"],
"references": [
// Этот проект зависит от @my-monorepo/utils
{ "path": "../utils" }
]
}
apps/web/tsconfig.json
{
"compilerOptions": {
"outDir": "./dist",
// ... другие compilerOptions, например, для React
},
"include": ["src"],
"exclude": ["node_modules"],
"references": [
// Веб-приложение зависит от UI-kit и Utils
{ "path": "../../packages/ui-kit" },
{ "path": "../../packages/utils" }
]
}

Теперь, когда вы запускаете tsc --build (или просто tsc -b) в корне, TypeScript умно соберет все проекты в правильном порядке, пересобирая только то, что нужно.

  1. “Cannot find module ’…’ or its corresponding type declarations.”

    • Причина: Чаще всего это проблема с paths, baseUrl или отсутствующими types/typeRoots в compilerOptions. Также может быть, что библиотека не имеет файлов деклараций (.d.ts), и вам нужно установить @types/название-пакета.
    • Решение: Проверьте paths и baseUrl. Убедитесь, что ваш бандлер также настроен для разрешения этих алиасов. Установите необходимые @types пакеты.
  2. “Property ‘x’ does not exist on type ’…’.”

    • Причина: Очень часто это из-за включенного strict: true (или strictNullChecks: true), когда вы пытаетесь получить доступ к потенциально null или undefined значению без проверки. Также может быть, что в lib не включены нужные библиотеки (например, DOM для браузерных API).
    • Решение: Используйте операторы ?. (optional chaining) и ?? (nullish coalescing), делайте проверки на null/undefined. Проверьте lib в compilerOptions.
  3. “Duplicate identifier ’…’.”

    • Причина: Ваши include или exclude настроены таким образом, что TypeScript пытается скомпилировать один и тот же файл дважды, или вы включаете скомпилированные .js файлы вместе с их .ts исходниками.
    • Решение: Уточните include и exclude. Убедитесь, что outDir находится в exclude.
  4. Проблемы с JSX/React.

    • Причина: Неправильно настроен jsx в compilerOptions, или lib не включает DOM.
    • Решение: Установите jsx в react-jsx (для React 17+) или react (для старых версий). Убедитесь, что lib содержит DOM.
  1. Настройка монорепо с extends и references:

    • Создайте корневой base-tsconfig.json с общими compilerOptions.
    • Создайте две папки: packages/shared-ui и apps/web.
    • В packages/shared-ui создайте tsconfig.json, который extends корневой, добавляет composite: true и declaration: true. Создайте простую функцию/компонент.
    • В apps/web создайте tsconfig.json, который extends корневой, добавляет jsx: "react-jsx" и references на shared-ui. Импортируйте компонент из shared-ui и используйте его.
    • Попробуйте скомпилировать все с помощью tsc -b.
  2. Алиасы с paths:

    • В вашем проекте (или в созданном на шаге 1) настройте baseUrl и paths так, чтобы можно было импортировать файлы из src/utils как @utils/* и из src/services как @services/*.
    • Добавьте соответствующие импорты в одном из ваших .ts файлов и убедитесь, что TypeScript их понимает.
    • (Бонусное задание): Если вы используете Webpack/Vite, настройте их, чтобы они также понимали эти алиасы.
  3. Эксперименты со strict и lib:

    • Возьмите небольшой .ts файл с несколькими переменными, которые могут быть null/undefined и без явных проверок.
    • Попробуйте скомпилировать его с strict: false, затем поменяйте на strict: true. Какие ошибки появились? Как их исправить?
    • Удалите DOM из lib в compilerOptions и попробуйте использовать document.getElementById(). Какие ошибки выдаст компилятор?

Всегда начинайте с strict: true! Это лучшая инвестиция в стабильность и поддерживаемость вашего кода. Поначалу будет больно, но это избавит вас от бесчисленных багов в будущем. Используйте extends для создания единой, хорошо поддерживаемой базовой конфигурации, а references — для оптимизации сборок в монорепозиториях. Ваш TypeScript-компилятор — ваш лучший друг, и tsconfig.json — это язык, на котором вы с ним общаетесь. Освойте его в совершенстве!