Введение: Почему 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) добавляются в каждый файл, где используется импорт. Это может немного увеличить размер кода,