esModuleInterop в tsconfig: Полный гайд для TypeScript

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему esModuleInterop — это не просто галочка в tsconfig

Если вы когда-нибудь писали на TypeScript и импортировали библиотеку вроде express или lodash, то наверняка сталкивались с моментом, когда компилятор ругается на import * as express from 'express'. Или, наоборот, вы спокойно используете import express from 'express', но в сгенерированном JavaScript-коде видите странную конструкцию module.exports.default. Что за магия? А магия (или её отсутствие) — это флаг esModuleInterop в вашем tsconfig.json.

Этот параметр — один из самых популярных вопросов на Stack Overflow (тысячи просмотров!), и неудивительно: без его понимания легко попасть в ловушку несовместимости модулей. В этом туториале мы не просто объясним теорию, а пройдём путь от проблемы до решения с живыми примерами. Поехали!

Проблема: Конфликт двух миров модулей

TypeScript — это надстройка над JavaScript, а JavaScript исторически имеет две основные системы модулей:

  • CommonJS (CJS) — стандарт Node.js, использует require() и module.exports. Пример: const express = require('express');
  • ES Modules (ESM) — современный стандарт, использует import и export. Пример: import express from 'express';

Проблема в том, что эти системы по-разному обрабатывают экспорт по умолчанию (default export). В ESM модуль может иметь один export default, и импортируется он как import name from 'module'. В CommonJS нет понятия "экспорт по умолчанию" — есть только объект module.exports, который может быть чем угодно.

Когда TypeScript компилирует ваш код в CommonJS (что часто нужно для Node.js), он должен решить: как представить import x from 'y' в виде require()? Без esModuleInterop TypeScript делает это буквально: создаёт module.exports.default, что ломает импорт библиотек, написанных под CommonJS.

Что такое esModuleInterop?

esModuleInterop — это флаг в tsconfig.json, который включает дополнительную совместимость между CommonJS и ES Modules. Когда он установлен в true, TypeScript генерирует вспомогательный код, который позволяет писать import foo from 'bar' даже для CommonJS-модулей, не имеющих default-экспорта.

По умолчанию в TypeScript 4.x и выше этот флаг равен true (если вы используете --init), но в старых проектах его часто выключали. Почему? Потому что он меняет поведение импорта, и без понимания это приводит к багам.

Как это выглядит в tsconfig.json

{
  "compilerOptions": {
    "esModuleInterop": true,
    // другие настройки...
  }
}

Включение этого флага также автоматически включает allowSyntheticDefaultImports, который разрешает импорт по умолчанию из модулей без реального default-экспорта на уровне типов.

Пошаговый туториал: от проблемы к решению

Давайте разберём ситуацию на реальном примере. Предположим, у нас есть простая библиотека на CommonJS:

// myLib.js (CommonJS)
module.exports = function greet(name) {
  return `Hello, ${name}!`;
};

Мы хотим использовать её в TypeScript-проекте. Создадим файл index.ts:

// index.ts
import greet from './myLib';
console.log(greet('World'));

Теперь посмотрим, что произойдёт при компиляции с разными настройками esModuleInterop.

Шаг 1: Без esModuleInterop (false)

Запустим компиляцию с "esModuleInterop": false. TypeScript выдаст ошибку:

Module '"myLib"' has no default export.

Чтобы это исправить, нам пришлось бы писать:

import * as greet from './myLib';
// или
import greet = require('./myLib');

Скомпилированный JavaScript будет выглядеть так:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const myLib_1 = require("./myLib");
console.log(myLib_1.default('World')); // Ошибка! myLib_1.default — undefined

Видите проблему? TypeScript сгенерировал вызов myLib_1.default, но наша CommonJS-библиотека не имеет свойства default. В результате — undefined и ошибка в рантайме.

Шаг 2: С esModuleInterop (true)

Теперь включим "esModuleInterop": true. Ошибка компиляции исчезнет, и мы можем спокойно писать import greet from './myLib'. Сгенерированный код станет таким:

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const myLib_1 = __importDefault(require("./myLib"));
console.log(myLib_1.default('World')); // Работает!

TypeScript добавил вспомогательную функцию __importDefault. Она проверяет: если модуль уже является ES-модулем (у него есть __esModule), то возвращает его как есть. Если нет (как в нашем случае с CommonJS), то оборачивает весь модуль в объект с ключом default. Теперь myLib_1.default — это наша функция greet, и всё работает.

Когда esModuleInterop действительно нужен?

Этот флаг критически важен в следующих сценариях:

  • Импорт старых npm-пакетов (например, express, lodash, moment), которые написаны на CommonJS и не имеют default-экспорта.
  • Смешанные проекты, где часть кода на ESM, часть на CJS.
  • Миграция с JavaScript на TypeScript — чтобы не переписывать все импорты.

Без esModuleInterop вам придётся использовать import * as X или import X = require(...), что менее удобно и может запутать новичков.

Подводные камни и нюансы

Несмотря на всю полезность, esModuleInterop не лишён тонкостей. Вот что нужно знать:

  • Размер бандла: Вспомогательные функции (__importDefault, __importStar) добавляются в каждый файл, где используется импорт. Это может немного увеличить размер кода,

Блоги

Рекомендаций по книгам