引言:为什么 esModuleInterop 不仅仅是 tsconfig 中的一个勾选项
\n\n如果你曾经使用 TypeScript 编写过代码,并导入过像 express 或 lodash 这样的库,那么你很可能遇到过编译器对 import * as express from 'express' 报错的情况。或者,你虽然可以正常使用 import express from 'express',但在生成的 JavaScript 代码中却看到了一个奇怪的构造 module.exports.default。这是什么魔法?其实,这个魔法(或者说它的缺失)就是 tsconfig.json 中的 esModuleInterop 标志。
这个参数是 Stack Overflow 上最热门的问题之一(有数千次浏览!),这并不奇怪:如果不理解它,很容易陷入模块不兼容的陷阱。在本教程中,我们不仅会解释理论,还会通过实际示例,带你从问题走向解决方案。我们开始吧!
\n\n问题:两个模块世界的冲突
\n\nTypeScript 是 JavaScript 的超集,而 JavaScript 历史上主要有两种模块系统:
\n- \n
- CommonJS (CJS) — Node.js 的标准,使用
require()和module.exports。示例:const express = require('express');\n - ES 模块 (ESM) — 现代标准,使用
import和export。示例:import express from 'express';\n
问题在于,这两种系统处理默认导出(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?
\n\nesModuleInterop 是 tsconfig.json 中的一个标志,用于启用 CommonJS 和 ES 模块之间的额外兼容性。当它设置为 true 时,TypeScript 会生成辅助代码,允许你为没有 default 导出的 CommonJS 模块编写 import foo from 'bar'。
在 TypeScript 4.x 及更高版本中,此标志默认为 true(如果你使用 --init),但在旧项目中,它经常被关闭。为什么?因为它会改变导入的行为,如果不理解,就会导致错误。
在 tsconfig.json 中如何配置
\n{\n "compilerOptions": {\n "esModuleInterop": true,\n // 其他设置...\n }\n}\n\n\n启用此标志还会自动启用 allowSyntheticDefaultImports,它允许从没有实际 default 导出的模块进行默认导入(在类型层面)。
分步教程:从问题到解决方案
\n\n让我们通过一个实际例子来分析这种情况。假设我们有一个基于 CommonJS 的简单库:
\n\n// myLib.js (CommonJS)\nmodule.exports = function greet(name) {\n return `Hello, ${name}!`;\n};\n\n\n我们想在 TypeScript 项目中使用它。创建一个 index.ts 文件:
// index.ts\nimport greet from './myLib';\nconsole.log(greet('World'));\n\n\n现在,让我们看看在 esModuleInterop 的不同设置下编译时会发生什么。
步骤 1:没有 esModuleInterop (false)
\n\n使用 "esModuleInterop": false 运行编译。TypeScript 会报错:
Module '"myLib"' has no d