本文 AI 產出,尚未審核

TypeScript 編譯與設定 – compilerOptions.esModuleInterop


簡介

在使用 TypeScript 開發 Node.js、React 或是其他 JavaScript 生態系的專案時,模組匯入的行為往往是最容易產生衝突的地方。
TypeScript 的編譯器(tsc)預設遵循 ES6 模組語法,而大多數既有的 npm 套件仍是以 CommonJSmodule.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.defaultundefined
  • 開啟 esModuleInterop 後,編譯器會自動在產生的程式碼前加入 __importDefault 輔助函式,將 CommonJS 的 module.exports 包裝成 { default: … },從而讓 import foo from "foo" 正常工作。

重點esModuleInterop 並不是把所有套件都改寫成 ES6,而是提供 一層兼容層,讓開發者可以使用更直觀的 import 語法。

2. esModuleInteropallowSyntheticDefaultImports 的關係

  • 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]

說明lodashmodule.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:使用 requireesModuleInterop 互補

// 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 的冗長。


常見陷阱與最佳實踐

陷阱 可能的症狀 解決方式
忘記同步 esModuleInteropallowSyntheticDefaultImports 型別檢查通過,但執行時 defaultundefined 確認 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 方式

最佳實踐

  1. 統一匯入風格
    在團隊內部約定:若套件支援 ES6 (export defaultexport const …) 則使用具名或 default 匯入;若是純 CommonJS,則使用 default import(前提是 esModuleInterop: true)。

  2. 保留 esModuleInteroptrue,除非有特殊需求
    這是目前大多數 TypeScript 專案的事實上標準設定,能兼容絕大多數 npm 套件。

  3. 使用 import type 只匯入型別
    為了避免產生不必要的執行時代碼,對於僅用於型別檢查的套件,使用 import type { Foo } from "foo"

  4. 檢查產出 JavaScript
    在疑難排解時,直接查看編譯後的 .js 檔,確認是否出現 __importDefault 包裝,這能快速判斷 esModuleInterop 是否正確生效。

  5. 配合 lint 規則
    使用 eslint-plugin-importtypescript-eslintno-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/awaitimport),在 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.esModuleInteropTypeScript 與 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 的世界裡寫出乾淨、易維護的程式碼!