Урок 39: Настройка tsconfig.json (основные опции)

Коллеги, добро пожаловать на предпоследний урок в низучении TypeScript. Если вы дошли до этого урока, то уже не новичок, а человек, обладающий солидным багажом знаний. Мы с вами прошли через дебри типов, научились создавать интерфейсы, дженерики и асинхронные функции. Но есть один файл, который является сердцем любого TypeScript-проекта, его ДНК и главным инструктором для компилятора. Сегодня, на Уроке 39, мы разберем его по косточкам. Речь пойдет о tsconfig.json.

Представьте, что компилятор TypeScript, это умный, но очень дотошный строитель. Без инструкций он либо откажется работать, либо будет строить «как получится». Файл tsconfig.json,  это и есть тот самый подробный технический план, чертеж, который мы ему даем. В нем мы указываем, в какую версию JavaScript компилировать наш код, насколько строгими быть к проверкам, куда складывать готовые файлы и множество других критически важных параметров.

Сегодня мы не будем пытаться объять необъятное (полный список опций занимает не одну страницу документации), а сфокусируемся на пяти самых важных и часто используемых полях: targetstrictmoduleoutDir и esModuleInterop. Понимание этих опций, это 90% успеха в настройке любого проекта. Давайте же закатаем рукава и приступим к настройке нашего «компиляторного» пульта управления.

target: Определяем цель компиляции

Поле target пожалуй, одна из первых и самых важных настроек, с которой вы столкнетесь. Оно говорит компилятору, в какую версию стандарта ECMAScript (это официальное название спецификации JavaScript) следует преобразовывать ваш TypeScript-код.

Зачем это нужно? TypeScript это надмножество JavaScript и он часто содержит возможности, которые еще не реализованы в браузерах или Node.js (например, декораторы, которые мы проходили). Компилятор берет ваш современный, красивый код на TypeScript и превращает его в тот JavaScript, который понимают ваши целевые среды выполнения (браузеры, серверы). Параметр target как раз и определяет, насколько «древний» должен быть этот итоговый JavaScript.

Допустимые значения для target это строки, соответствующие версиям ES: "ES3""ES5""ES6" (или синоним "ES2015"), "ES2017""ES2020""ES2022" и так далее, вплоть до "ESNext" (последняя стабильная версия). Если вы пишете код для старых браузеров, таких как Internet Explorer 11, вам придется выбрать "ES5". Это гарантирует, что даже такие конструкции, как стрелочные функции () => {} и class, будут преобразованы в функции-конструкторы и function.

Давайте посмотрим на практическом примере. Представьте, у нас есть такой TypeScript-код:

typescript
// Файл: src/index.ts
const greet = (name: string): string => `Hello, ${name}!`;

class Person {
  constructor(public name: string) {}
  greet() {
    return greet(this.name);
  }
}

const alice = new Person("Alice");
console.log(alice.greet());

Теперь посмотрим, во что это скомпилируется с разными целями.

Цель: "ES5"

javascript
// Файл: dist/index.js (при настройке outDir, о котором позже)
"use strict";
var greet = function (name) {
    return "Hello, " + name + "!";
};
var Person = /** @class */ (function () {
    function Person(name) {
        this.name = name;
    }
    Person.prototype.greet = function () {
        return greet(this.name);
    };
    return Person;
}());
var alice = new Person("Alice");
console.log(alice.greet());

Компилятор преобразовал стрелочную функцию в обычную функцию-выражение, а класс в конструктор с прототипами. Это код, который поймет даже IE11.

Цель: "ES2015" или выше

javascript
// Файл: dist/index.js
"use strict";
const greet = (name) => `Hello, ${name}!`;
class Person {
    constructor(name) {
        this.name = name;
    }
    greet() {
        return greet(this.name);
    }
}
const alice = new Person("Alice");
console.log(alice.greet());

Здесь компилятор оставил современные конструкции на месте, так как ES2015 и так поддерживает и стрелочные функции и классы. Этот код будет работать в современных браузерах и версиях Node.js.

Как выбрать target?

  • Для веб-проектов. Ориентируйтесь на браузеры, которые должны поддерживать ваши пользователи. babeljs.io/docs/en/caveats. Часто выбирают "ES2015" или "ES2017" как хороший баланс между современностью и поддержкой.

  • Для проектов на Node.js. Смело ставьте версию, соответствующую той версии Node.js, на которой вы запускаете проект (например, "ES2022" для Node.js 18+). Node.js очень быстро догоняет стандарты.

Практическая задача 1:
Создайте файл tsconfig.json в корне вашего тестового проекта. Пока что он может быть пустым {}. Создайте файл index.ts с примером класса и стрелочной функции, как выше. Скомпилируйте проект с помощью команды tsc (она найдет tsconfig.json автоматически). По умолчанию target "ES3", посмотрите на результат. Теперь явно установите в tsconfig.json значение "target": "ES2017" и скомпилируйте снова. Сравните выходные файлы.

strict: Режим строгой типизации

Если бы мне пришлось выбрать одну-единственную настройку, которая делает из TypeScript по-настоящему мощный и надежный инструмент, это была бы опция strict. Включение этого флага, это как переход с «велосипедных тренировочных колес» на управление гоночным болидом. Вы получаете максимальный контроль и безопасность, но и ответственность за качество кода возрастает.

strict это главный выключатель для целого семейства опций строгой проверки типов. Когда вы устанавливаете "strict": true в вашем tsconfig.json, вы включаете сразу несколько под-опций, таких как noImplicitAnystrictNullChecksstrictFunctionTypes и многие другие. Давайте разберем самые важные из них, чтобы понять, что же именно мы включаем.

noImplicitAny: Без неявного any
Эта опция не позволяет компилятору молча выводить тип any в ситуациях, когда тип не может быть определен автоматически. Тип any это аварийный люк, отключающий проверку типов. Он полезен в крайних случаях, но его неконтролируемое использование сводит на нет все преимущества TypeScript.

  • Без noImplicitAny (или при "strict": false):

    typescript
    function greet(name) { // Параметр 'name' неявно получает тип 'any'
      return `Hello, ${name}!`;
    }

    Компилятор промолчит, но вы потеряли безопасность типов для аргумента name.

  • С noImplicitAny": true:

    typescript
    function greet(name) { // ОШИБКА: Parameter 'name' implicitly has an 'any' type.
      return `Hello, ${name}!`;
    }

    Компилятор заставит вас явно указать тип: function greet(name: string) { ... }.

strictNullChecks: Строгая проверка на null и undefined
Это, возможно, одна из самых полезных строгих опций. Она не позволяет значениям null и undefined быть присвоены любым типам по умолчанию. Это помогает избежать знаменитой ошибки «Cannot read properties of null/undefined».

  • Без strictNullChecks:

    typescript
    let userName: string = "Max";
    userName = null; // ОК! Это может быть источником будущих ошибок.
    
    function getElement(id: string): HTMLElement {
      // Мы предполагаем, что элемент всегда есть, но это не так!
      return document.getElementById(id); // HTMLElement | null
    }
    const el = getElement("not-exist");
    el.innerHTML = "Hi"; // Возможна ошибка времени выполнения!
  • С strictNullChecks": true:

    typescript
    let userName: string = "Max";
    userName = null; // ОШИБКА: Type 'null' is not assignable to type 'string'.
    
    // Теперь мы должны явно указать, что значение может быть null.
    let userNameOrNull: string | null = "Max";
    userNameOrNull = null; // OK
    
    // И функция должна отражать эту возможность.
    function getElement(id: string): HTMLElement | null {
      return document.getElementById(id);
    }
    const el = getElement("not-exist");
    if (el) { // Теперь компилятор заставляет нас проверить наличие элемента!
      el.innerHTML = "Hi"; // Безопасно!
    }

Включать ли strict режим? Мой ответ ДА, ИМЕННО СЕЙЧАС. Лучше привыкать писать качественный, типобезопасный код с самого начала. Возможно, сначала будет больше ошибок компиляции, но каждая из них, это потенциальный баг, который вы поймали еще до запуска программы.

Практическая задача 2:
В вашем tsconfig.json установите "strict": true. Попробуйте написать функцию, которая принимает два аргумента без указания типов. Убедитесь, что компилятор ругается. Затем попробуйте присвоить переменной с типом string значение null. Поэкспериментируйте, исправляя ошибки, которые показывает компилятор.

module: Система модулей на выходе

В современной JavaScript-разработке код организуется в модули. TypeScript поддерживает различные синтаксисы для работы с модулями (importexportexport default). Опция module указывает компилятору, в какой системе модулей генерировать итоговый JavaScript-код.

Почему это важно? Потому что среда, в которой выполняется ваш код (браузер, Node.js), понимает определенные форматы модулей. Например, браузеры исторически понимали только загрузку через теги <script>, но с появлением ES-модулей теперь поддерживают нативный import/export. Node.js долгое время использовал свою систему модулей CommonJS, но сейчас также поддерживает ES-модули.

Основные значения для module:

  • "CommonJS" (или "cjs"): Стандарт для Node.js. Модули подключаются через require(), а экспортируются через module.exports или exports.

  • "ES6" / "ES2015" (или "es6""es2015"): Нативные JavaScript-модули. Синтаксис import/export остается как есть.

  • "UMD""AMD""System": Более старые или специализированные форматы, сейчас используются реже.

Давайте посмотрим на преобразование.

Исходный TypeScript-код:

typescript
// math.ts
export const PI = 3.14;
export function square(x: number): number {
  return x * x;
}
export default class Calculator {
  // ... методы калькулятора
}
typescript
// index.ts
import Calculator, { PI, square } from './math';
const calc = new Calculator();
console.log(PI, square(calc.result));

С "module": "CommonJS":

javascript
// math.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PI = 3.14;
exports.square = square;
class Calculator {
}
exports.default = Calculator;

// index.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const math_1 = __importDefault(require("./math"));
console.log(math_1.default.PI, (0, math_1.default.square)(calc.result)); // Обратите внимание на обращение к свойствам!

Компилятор преобразовал import/export в вызовы require и module.exports.

С "module": "ES2015":

javascript
// math.js
export const PI = 3.14;
export function square(x) {
    return x * x;
}
export default class Calculator {
}

// index.js
import Calculator, { PI, square } from './math';
const calc = new Calculator();
console.log(PI, square(calc.result));

Синтаксис остался практически неизменным. Такой код можно использовать в современных браузерах (с атрибутом <script type="module">) или в Node.js (с расширением .mjs или при указании "type": "module" в package.json).

Как выбрать?

  • Для Node.js-проектов: Если вы не используете нативные ES-модули, выбирайте "CommonJS".

  • Для фронтенд-проектов, собираемых бандлерами (Webpack, Vite, Parcel): Бандлеры умеют работать с разными форматами, но часто "ES2015" является хорошим выбором, так как позволяет бандлеру лучше проводить tree-shaking (удаление неиспользуемого кода).

  • Для фронтенда без бандлера (нативные ES-модули в браузере): Используйте "ES2015".

outDir: Держим порядок в проекте

Когда вы пишете код на TypeScript, вы, как правило, храните его в одной директории (часто src/), а скомпилированные JavaScript-файлы должны попадать в другую (например, dist/ или build/). Это разумное разделение: вы не хотите, чтобы ваши исходники перемешивались с артефактами сборки. Опция outDir как раз и указывает компилятору, в какую папку складывать результаты своей работы.

Без указания outDir скомпилированные файлы .js (а также .d.ts, если вы генерируете декларации) будут созданы прямо рядом с исходными файлами .ts. Это быстро приводит к беспорядку.

Пример настройки:

json
{
  "compilerOptions": {
    "target": "ES2017",
    "module": "CommonJS",
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src/**/*"] // Скажем компилятору, где искать .ts файлы
}

С такой конфигурацией, если у вас есть файл src/utils/helpers.ts, после компиляции появится файл dist/utils/helpers.js. Структура папок внутри src/ будет полностью повторена внутри outDir.

Практическая задача 3:
Создайте в вашем проекте папку src. Переместите туда ваш файл index.ts. В tsconfig.json добавьте опцию "outDir": "./dist" и секцию "include": ["src/**/*"]. Запустите компиляцию командой tsc. Убедитесь, что в корне проекта появилась папка dist с вашим скомпилированным index.js внутри.

esModuleInterop: Решаем проблемы совместимости модулей

А вот мы и подошли к, возможно, самой «магической» и неочевидной опции. esModuleInterop была введена, чтобы решить фундаментальную проблему несовместимости между модулями CommonJS и ES-модулями.

В чем проблема? Модуль CommonJS, экспортируемый через module.exports и модуль ES, экспортируемый через export default это концептуально разные вещи. CommonJS модуль это один «мешок с экспортами», а ES-модуль это пространство имен с именованными экспортами и одним «основным» default.

До появления esModuleInterop импорт CommonJS модуля в TypeScript выглядел так:

typescript
import * as express from 'express'; // Старый синтаксис
const app = express();

Или так, если библиотека поддерживала «синтаксис по умолчанию»:

typescript
import express from 'express'; // Могло работать, а могло и нет!

Проблема в том, что не все CommonJS-библиотеки можно корректно импортировать как ES-модули с default-экспортом. Включение "esModuleInterop": true делает две важные вещи:

  1. Она позволяет использовать синтаксис import moment from 'moment'; для импорта CommonJS-модулей. Это называется «synthetic default imports».

  2. Она добавляет вспомогательные функции в скомпилированный код, которые обеспечивают совместимость, проверяя, является ли импортируемый модуль ES-модулем или CommonJS-модулем и соответствующим образом оборачивая его.

С "esModuleInterop": false (по умолчанию в старых версиях):

typescript
// Наш код (TS)
import * as fs from 'fs'; // Так всегда "безопасно"
// ИЛИ, если повезет, для некоторых библиотек:
// import fs from 'fs'; // Может привести к ошибке!

С "esModuleInterop": true (рекомендуется):

typescript
// Наш код (TS)
import fs from 'fs'; // Теперь это работает корректно!
// Это просто удобнее и семантически правильнее.

Что происходит под капотом? Компилятор сгенерирует примерно такой код:

javascript
// Сгенерированный JS (с "esModuleInterop": true)
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
const fs_1 = __importDefault(require("fs"));
// Теперь мы используем fs_1.default

Эта вспомогательная функция __importDefault проверяет, является ли модуль, возвращенный require('fs'), ES-модулем (у него есть свойство __esModule). Если да, она просто возвращает его. Если нет (как в случае с большинством CommonJS-модулей), она оборачивает его в объект со свойством default. Это позволяет нам использовать красивый и единообразный синтаксис import library from 'library'.

Рекомендация: Всегда устанавливайте "esModuleInterop": true в новых проектах. Это избавит вас от головной боли и сделает код чище.

Практическая задача 4:
Убедитесь, что в вашем tsconfig.json установлена опция "esModuleInterop": true. Попробуйте установить какую-нибудь популярную CommonJS-библиотеку через npm (например, npm install lodash). Теперь попробуйте импортировать ее в ваш index.ts двумя способами:

  1. import * as _ from 'lodash';

  2. import _ from 'lodash';

Убедитесь, что оба способа работают без ошибок компиляции (благодаря esModuleInterop). Посмотрите в сгенерированный файл dist/index.js и найдите там вспомогательную функцию __importDefault.

Пример полного tsconfig.json

Давайте соберем все изученные сегодня опции в одном файле-примере, который можно использовать как надежную основу для многих проектов.

json
{
  // Этот файл в формате JSON, поэтому комментарии через "//" недопустимы в реальном файле!
  // Они здесь только для пояснения. Удалите их в рабочем проекте.
  "compilerOptions": {
    /* Базовая настройка компиляции */
    "target": "ES2020",        // Современные браузеры и Node.js 14+
    "module": "CommonJS",      // Стандарт для Node.js. Для фронта можно "ES2015"
    "outDir": "./dist",        // Складываем скомпилированные файлы сюда
    "rootDir": "./src",        // Исходники лежат здесь

    /* Строгие проверки типов - ЗАЛОГ БЕЗОПАСНОСТИ */
    "strict": true,            // Включает все строгие проверки
    "esModuleInterop": true,   // Корректная работа с CommonJS-модулями
    "skipLibCheck": true,      // Ускоряет компиляцию, пропуская проверку типов в node_modules
    "forceConsistentCasingInFileNames": true, // Защита от проблем с регистром букв в разных ОС

    /* Дополнительные полезные опции */
    "declaration": true,       // Генерировать .d.ts файлы (полезно для библиотек)
    "sourceMap": true,         // Генерировать .map файлы для отладки
    "removeComments": false,    // Удалять комментарии из выходного кода
    "noEmitOnError": true      // Не генерировать файлы при наличии ошибок типов
  },
  "include": ["src/**/*"],     // Какие файлы компилировать
  "exclude": ["node_modules", "dist", "**/*.test.ts"] // Какие файлы игнорировать
}

Этот конфиг, отличная отправная точка. Вы можете копировать его в новые проекты и адаптировать под свои нужды.

Заключение

Вы только что получили ключи от главного пульта управления TypeScript-проектом. Понимание tsconfig.json это признак зрелого разработчика. Вы больше не просто используете язык, вы настраиваете его под свои конкретные задачи.

Вы узнали, что:

  1. target определяет, насколько современный JavaScript будет на выходе.

  2. strict ваш главный союзник в борьбе с ошибками, включающий максимальный уровень проверки типов.

  3. module указывает систему модулей для итогового кода.

  4. outDir помогает поддерживать чистоту в проекте, отделяя исходники от артефактов сборки.

  5. esModuleInterop решает давние проблемы совместимости между CommonJS и ES-модулями, делая импорт более удобным.

Создайте тестовый проект, попробуйте разные настройки, посмотрите, как меняется выходной код. Чем лучше вы понимаете tsconfig.json, тем более эффективным и приятным становится процесс разработки на TypeScript.

Если вы хотите структурировать свои знания от самых основ до продвинутых тем, рекомендую пройти полный курс с уроками по TypeScript для начинающих.

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий