Понимание esModuleInterop в tsconfig.json: как исправить импорт CommonJS модулей в TypeScript
Что такое esModuleInterop и почему возникает ошибка?
Проблема, с которой вы столкнулись, связана с различиями между модульными системами в JavaScript/TypeScript. Давайте разберёмся по порядку.
Express — это библиотека, написанная на JavaScript и распространяемая как CommonJS модуль. В CommonJS экспорт обычно выглядит так:
// CommonJS (как написана express)
module.exports = function createApp() { ... }
TypeScript (и современный JavaScript) использует ES модули (ESM), где экспорт/импорт работают иначе. Когда TypeScript видит модуль, который использует module.exports = ..., он интерпретирует это как одиночный экспорт (export =). Для импорта такого модуля в TypeScript нужно использовать специальный синтаксис:
// Правильный импорт для модулей с export =
import express = require('express');
Однако это неудобно, и большинство разработчиков хотят писать:
// Привычный синтаксис ES модулей
import express from 'express';
Именно здесь на помощь приходит esModuleInterop.
Как работает esModuleInterop
Когда вы включаете "esModuleInterop": true в tsconfig.json, TypeScript добавляет вспомогательные функции в скомпилированный JavaScript-код, которые эмулируют поведение ES модулей для CommonJS модулей. Технически это делает две вещи:
- Создаёт "синтетический" default export из модуля, который его не имеет (как express).
- Разрешает импорт
import * asиimport x fromдля CommonJS модулей.
Вот что происходит "под капотом". Без esModuleInterop код:
import express from 'express';
...скомпилируется в:
const express_1 = require("express");
// Ошибка! express_1.default не существует
А с включённым esModuleInterop:
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const express_1 = __importDefault(require("express"));
// Теперь express_1.default — это то же самое, что module.exports
То есть TypeScript оборачивает импортируемый модуль в объект с полем default, что позволяет использовать синтаксис import x from 'module'.
Правильная настройка tsconfig.json
Рекомендуемый способ — всегда включать esModuleInterop в современных проектах. Также настоятельно рекомендуется включить "allowSyntheticDefaultImports": true (хотя esModuleInterop автоматически включает его).
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true, // Добавьте это
"allowSyntheticDefaultImports": true // Обычно включается автоматически
}
}
После этого вы сможете писать:
import express from 'express';
const app = express(); // Работает!
Альтернативные варианты импорта (без esModuleInterop)
Если по каким-то причинам вы не хотите включать esModuleInterop, вы можете использовать:
// Вариант 1: Импорт всего модуля как пространства имён
import * as express from 'express';
// Вариант 2: Использование синтаксиса require (не рекомендуется)
import express = require('express');
Но вариант с import * as может работать не всегда корректно — в некоторых модулях export = ведёт себя иначе. Поэтому проще и надёжнее включить esModuleInterop.
Важное замечание про allowSyntheticDefaultImports
Есть ещё один флаг — allowSyntheticDefaultImports. Он только разрешает TypeScript не выдавать ошибку, но не добавляет вспомогательный код при компиляции. Если вы используете allowSyntheticDefaultImports без esModuleInterop, TypeScript не будет ругаться, но скомпилированный JavaScript может упасть в рантайме, потому что require('express').default будет undefined.
esModuleInterop же и подавляет ошибку, и генерирует правильный код.
Итог
- Проблема возникает из-за несовместимости CommonJS (express) и ES модулей (TypeScript).
esModuleInterop: true— это "волшебная палочка", которая заставляет CommonJS модули вести себя как ES модули.- Всегда включайте его в проектах, где вы используете библиотеки из npm (особенно старые).
- Не путайте с
allowSyntheticDefaultImports— он только для типов, аesModuleInteropменяет скомпилированный код.
Дополнительный взгляд: когда esModuleInterop может быть не нужен
Есть ситуации, когда вы можете не использовать esModuleInterop:
- Чистые ES модули: Если все ваши зависимости — это современные библиотеки, написанные как ES модули (например, некоторые новые версии пакетов), то
esModuleInteropне нужен. Но таких библиотек пока меньшинство. - Node.js с "type": "module": Если вы используете Node.js с ESM (установив
"type": "module"в package.json и"module": "ESNext"в tsconfig), тоesModuleInteropвсё ещё может понадобиться для старых библиотек. - Микрооптимизация: В редких случаях, когда каждый байт кода важен (например, для библиотек), вы можете отказаться от
esModuleInterop, чтобы не генерировать вспомогательный код. Но для обычных приложений это не имеет значения.
Альтернативный подход — использовать moduleResolution: "node16" или "nodenext" в комбинации с "module": "node16". Эти настройки лучше понимают современную модульную систему Node.js и могут автоматически обрабатывать импорты без дополнительных флагов. Однако они требуют более строгой структуры проекта (например, явного указания расширений файлов при импорте).
В большинстве случаев рекомендую просто включить esModuleInterop и не задумываться — это стандартная практика в TypeScript-сообществе.