Avatar ·

Понимание esModuleInterop в tsconfig.json: как исправить импорт CommonJS модулей в TypeScript

Всем привет! Я новичок в TypeScript и столкнулся с непонятной для меня проблемой при настройке проекта. У меня есть Node.js проект, и я использую популярную библиотеку `express`. Я установил её через npm и также установил типы (`@types/express`). Мой файл `tsconfig.json` выглядит так: ```json { "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true } } ``` Когда я пытаюсь импортировать express вот так: ```typescript import express from 'express'; ``` TypeScript выдаёт ошибку: `Module '"/path/to/node_modules/@types/express/index"' has no default export.` или что-то вроде `This module is declared with 'export =' and can only be used with a default import when using the 'esModuleInterop' flag.` Я читал про флаг `esModuleInterop`, но не до конца понимаю, что он делает и почему это вообще проблема. Почему я не могу просто импортировать express так, как привык в JavaScript? Какая разница между `import * as express from 'express'` и `import express from 'express'`? И как правильно настроить tsconfig, чтобы всё работало без ошибок? Буду благодарен за подробное объяснение с примерами. <<>>

Что такое 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-сообществе.