Перейти к содержимому

Module Resolution: node vs bundler, esModuleInterop

Болезненная тема: «у меня всё работает, у тебя — нет» обычно про moduleResolution и расширения. Красный флаг — не знать, что nodenext требует расширений в импортах, и путать esModuleInterop с allowSyntheticDefaultImports.

Какую стратегию выбрать:

// Сводка
// 'node' — классика TS до 4.7, ищет 'pkg' через node_modules без поддержки exports map
// 'node16' / 'nodenext' — современная Node ESM/CJS:
// полная поддержка package.json#exports, conditional exports,
// ОБЯЗАТЕЛЬНЫ расширения './util.js' даже из .ts
// 'bundler' — TS 5.0+: поведение, как у Vite/esbuild/webpack:
// exports map поддержан, расширения НЕ обязательны
// tsconfig.json для современного веб-проекта (Vite/Next):
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true, // import fs from 'fs' для cjs-модулей
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true
}
}
// tsconfig.json для Node-библиотеки на ESM:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"esModuleInterop": true
}
}

Что меняет esModuleInterop:

// без esModuleInterop:
import * as fs from 'fs';
fs.readFileSync('a');
// с esModuleInterop:
import fs from 'fs';
fs.readFileSync('a'); // CJS-модуль импортируется как default

Итог: Веб с бандлером — moduleResolution: bundler; чистая Node ESM-библиотека — nodenext с расширениями .js в импортах.