TypeScript 編譯與設定 – compilerOptions.esModuleInterop
簡介
在使用 TypeScript 開發 Node.js、React 或是其他 JavaScript 生態系的專案時,模組匯入的行為往往是最容易產生衝突的地方。
TypeScript 的編譯器(tsc)預設遵循 ES6 模組語法,而大多數既有的 npm 套件仍是以 CommonJS(module.exports = …)或 AMD 的形式發佈。當我們直接使用 import foo from "foo" 來匯入這類套件時,往往會看到類似「foo has no default export」的錯誤訊息。
compilerOptions.esModuleInterop 正是為了解決這個兼容性問題而設計的。開啟此選項後,編譯器會在產生的 JavaScript 中自動加入一層 interop(互操作)代碼,使 CommonJS 模組可以像 ES Module 一樣使用 default 匯入,從而大幅降低開發者在模組切換時的阻力。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶入實務應用情境,完整介紹 esModuleInterop 的使用方式與注意事項,讓初學者也能快速上手,同時提供中階開發者更深入的調校技巧。
核心概念
1. 為什麼會需要 esModuleInterop?
| 模組系統 | 匯入方式 | 產生的 JavaScript(未開啟 esModuleInterop) |
|---|---|---|
CommonJS (module.exports = …) |
import foo from "foo" |
var foo_1 = require("foo");(無 default) |
ESM (export default …) |
import foo from "foo" |
var foo_1 = __importDefault(require("foo"));(有 default) |
- CommonJS 匯出的是一個 物件,而非 ES6 所謂的 default export。
- TypeScript 若不做額外處理,會直接把
require的結果指派給變數,導致foo.default為undefined。 - 開啟
esModuleInterop後,編譯器會自動在產生的程式碼前加入__importDefault輔助函式,將 CommonJS 的module.exports包裝成{ default: … },從而讓import foo from "foo"正常工作。
重點:
esModuleInterop並不是把所有套件都改寫成 ES6,而是提供 一層兼容層,讓開發者可以使用更直觀的import語法。
2. esModuleInterop 與 allowSyntheticDefaultImports 的關係
allowSyntheticDefaultImports只在 型別檢查 階段允許使用default匯入,不會改變編譯輸出。esModuleInterop不僅允許型別檢查,還會在 編譯結果 中加入互操作代碼。
建議:若只想在型別檢查階段避免錯誤,可開啟
allowSyntheticDefaultImports;若希望真正產生可執行的互操作程式碼,則必須開啟esModuleInterop(此選項會自動啟用allowSyntheticDefaultImports)。
3. esModuleInterop 的實作原理
編譯器會在每個 import foo from "foo" 之上插入以下輔助函式(簡化版):
function __importDefault(mod) {
return (mod && mod.__esModule) ? mod : { default: mod };
}
然後將原本的 require 呼叫改寫為:
var foo_1 = __importDefault(require("foo"));
如此一來,foo_1.default 就是原本 CommonJS 匯出的實體,而 TypeScript 產生的變數 foo 會指向 foo_1.default,達成「看起來像 ES6 default import」的效果。
程式碼範例
以下示範 5 個實用範例,說明在不同情境下如何透過 esModuleInterop 正確匯入套件。
範例 1:匯入 lodash(CommonJS)
// tsconfig.json
{
"compilerOptions": {
"esModuleInterop": true,
"target": "ES2019",
"module": "commonjs"
}
}
// foo.ts
import _ from "lodash"; // 直接使用 default import
// 使用 lodash 的函式
const arr = [1, 2, 3];
const doubled = _.map(arr, n => n * 2);
console.log(doubled); // [2,4,6]
說明:
lodash以module.exports = _匯出。開啟esModuleInterop後,我們可以直接寫import _ from "lodash",而不必改成import * as _ from "lodash"。
範例 2:匯入 express(CommonJS)搭配型別檔
import express from "express";
const app = express(); // express 本身是一個函式
app.get("/", (req, res) => {
res.send("Hello, world!");
});
app.listen(3000, () => console.log("Server running on :3000"));
註:
express的型別檔 (@types/express) 已經宣告了export = express;,若未開啟esModuleInterop,必須改寫成import * as express from "express"。開啟後即可使用更直觀的default import。
範例 3:同時使用 default 與具名匯入
// library.js (CommonJS)
module.exports = {
default: function greet(name) { return `Hi, ${name}`; },
version: "1.0.0"
};
// app.ts
import greet, { version } from "./library";
console.log(greet("Alice")); // Hi, Alice
console.log(`Library version: ${version}`); // Library version: 1.0.0
重點:即使原始模組同時提供
default與具名屬性,esModuleInterop仍會正確映射,使兩者都能透過 ES6 語法取得。
範例 4:使用 require 與 esModuleInterop 互補
// tsconfig 中仍保持 esModuleInterop: true
// 用 require 讀取 JSON 檔(Node 內建支援)
import config from "./config.json"; // JSON 被視為 ES6 模組
// 仍然可以混用 require
const path = require("path"); // CommonJS
console.log(path.join(__dirname, "data"));
說明:
esModuleInterop只影響 import 語句,對 require 本身不產生副作用。這讓你可以在同一檔案中自由混用兩種匯入方式。
範例 5:在 React 專案中使用第三方 UI 套件
// tsconfig.json (React 專案)
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"target": "ES2020",
"module": "esnext"
}
}
// MyComponent.tsx
import React from "react";
import Button from "antd/lib/button"; // Ant Design 使用 CommonJS 包裝
export default function MyComponent() {
return <Button type="primary">點我</Button>;
}
實務觀點:許多 UI 套件(如 Ant Design、Material‑UI)在舊版仍以 CommonJS 包裝。開啟
esModuleInterop後,開發者可以直接使用default import,減少import * as的冗長。
常見陷阱與最佳實踐
| 陷阱 | 可能的症狀 | 解決方式 |
|---|---|---|
忘記同步 esModuleInterop 與 allowSyntheticDefaultImports |
型別檢查通過,但執行時 default 為 undefined |
確認 tsconfig.json 中 兩者同時開啟,或僅使用 esModuleInterop(它會自動開啟前者) |
在已啟用 esModuleInterop 的專案中仍使用 import * as … |
產生多層 default 包裝,導致 foo.default.default |
盡量改寫為 default import;若必須保留舊寫法,使用 import * as foo from "foo" 並自行存取 foo.default |
同時使用 module: "commonjs" 與 target: "ESNext",但未設定 moduleResolution: "node" |
編譯器找不到模組或產生錯誤的 require 路徑 |
在 tsconfig.json 明確加入 "moduleResolution": "node" |
對於純 JSON、CSS、圖片等資源使用 default import |
若未安裝相應的 module declaration,會出現「Cannot find module」錯誤 | 安裝 @types/node 或自行宣告 declare module "*.json" 等類型定義 |
在 Node.js 原生 ESM 專案("type": "module")中仍使用 CommonJS |
產生 SyntaxError: Cannot use import statement outside a module |
若要在 ESM 專案中使用 esModuleInterop,請改為 "module": "ESNext" 並使用 import,或僅保留 CommonJS 方式 |
最佳實踐
統一匯入風格
在團隊內部約定:若套件支援 ES6 (export default或export const …) 則使用具名或 default 匯入;若是純 CommonJS,則使用default import(前提是esModuleInterop: true)。保留
esModuleInterop為true,除非有特殊需求
這是目前大多數 TypeScript 專案的事實上標準設定,能兼容絕大多數 npm 套件。使用
import type只匯入型別
為了避免產生不必要的執行時代碼,對於僅用於型別檢查的套件,使用import type { Foo } from "foo"。檢查產出 JavaScript
在疑難排解時,直接查看編譯後的.js檔,確認是否出現__importDefault包裝,這能快速判斷esModuleInterop是否正確生效。配合 lint 規則
使用eslint-plugin-import或typescript-eslint的no-restricted-imports來強制使用default import,減少import * as的混用。
實際應用場景
1. 微服務平台的共用函式庫
在企業內部的微服務架構中,常會有 共用工具函式庫(如 @company/utils)以 CommonJS 發布。若每個服務都必須寫成 import * as utils from "@company/utils",會造成代碼風格不一致且難以閱讀。開啟 esModuleInterop 後,所有服務只需要:
import utils from "@company/utils";
同時保留型別安全,減少重構成本。
2. 前端 React/Next.js 專案的 UI 套件
Next.js 預設使用 ESModules,但許多 UI 套件仍是 CommonJS(尤其是舊版)。若未開啟 esModuleInterop,開發者需要在每個元件裡寫:
import * as Button from "antd/lib/button";
這不僅冗長,還會在 TypeScript 中產生 any 警告。加入 esModuleInterop 後,只要:
import Button from "antd/lib/button";
即可直接使用,編譯與執行都更順暢。
3. 跨平台 CLI 工具
Node.js CLI 常用 commander, chalk 等套件,這些套件仍以 CommonJS 發布。為了讓程式碼保持 ES6 風格(如 async/await、import),在 tsconfig.json 中加入:
{
"compilerOptions": {
"esModuleInterop": true,
"target": "ES2020",
"module": "commonjs"
}
}
即可在 CLI 主入口使用:
#!/usr/bin/env node
import { Command } from "commander";
import chalk from "chalk";
const program = new Command();
program.version("1.0.0").parse(process.argv);
console.log(chalk.green("CLI ready!"));
此方式讓 CLI 既保留 CommonJS 的執行環境,又能寫出符合現代語法的 TypeScript。
總結
compilerOptions.esModuleInterop是 TypeScript 與 CommonJS 互操作的關鍵設定,它會在編譯階段自動加入__importDefault包裝,使import foo from "foo"能正確對應到module.exports = foo。- 與
allowSyntheticDefaultImports不同,esModuleInterop不僅在型別檢查階段允許 default 匯入,還會產生 可執行的互操作代碼,因此在實務專案中更具價值。 - 開啟此選項後,開發者可以統一使用 default import,降低代碼冗長、提升可讀性,特別適合大型團隊與混合模組的環境。
- 常見陷阱包括未同步
allowSyntheticDefaultImports、混用import * as、以及在純 ESM 專案中仍使用 CommonJS。遵守最佳實踐(統一匯入風格、檢查產出代碼、配合 lint)能有效避免這些問題。 - 在微服務、React/Next.js 前端、Node.js CLI 等真實場景中,
esModuleInterop已成為 降低模組兼容成本的利器,幾乎是所有新建 TypeScript 專案的事實上標準設定。
最後提醒:即使
esModuleInterop帶來便利,仍應維持對套件本身匯出方式的基本了解,這樣在面對特殊或自訂模組時,才能快速定位問題、選擇正確的匯入寫法。祝你在 TypeScript 的世界裡寫出乾淨、易維護的程式碼!