本文 AI 產出,尚未審核

TypeScript 編譯與設定 – compilerOptions.module


簡介

在使用 TypeScript 開發大型應用程式時,編譯器的設定往往決定了最終產出的 JavaScript 能否順利執行於目標環境(Node、瀏覽器、或是各種模組打包工具)。
compilerOptions.module 就是其中最關鍵的選項之一,它告訴 TypeScript 要以何種模組系統產生程式碼,從而影響 import / export 的轉譯方式、相依性解析方式以及與其他生態系工具的相容性。

如果對 module 的意圖不清楚,常會出現「找不到模組」或「執行時 require 為 undefined」等錯誤,甚至會導致打包體積不如預期。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握 module 設定的使用方式,讓你在 Node、前端或是庫 (library) 開發 時,都能選擇最適合的模組目標。


核心概念

1. 為什麼需要 module

TypeScript 本身支援 ES 模組語法 (import / export),但瀏覽器與 Node 在不同時期只支援不同的模組系統。
module 讓編譯器把 TypeScript 的 ES 模組 轉譯 為:

模組系統 產出範例 典型使用情境
CommonJS module.exports = ...require() Node.js、舊版工具
ES6 / ES2015 exportimport (原生) 現代瀏覽器、Webpack / Rollup
ESNext 保留 ES2020+ 語法 (如 import.meta) 使用最新瀏覽器或支援的打包器
AMD define([...], function (...) {...}) RequireJS
System System.register([...], function (exports) {...}) SystemJS
UMD 同時支援 AMD、CommonJS、全域變數 共享庫、CDN 發佈
None 不產生任何模組語法,僅保留檔案間的相對路徑 只做型別檢查,交給外部工具自行處理

重點:選擇哪種模組,必須與 目標執行環境target)以及 打包工具(Webpack、Rollup、esbuild…)保持一致。


2. tsconfig.json 中的設定範例

{
  "compilerOptions": {
    "target": "ES2019",          // 產出語法的最低版本
    "module": "ESNext",          // 產出 ES 模組
    "moduleResolution": "node", // 依照 Node 解析規則尋找模組
    "outDir": "./dist",          // 編譯後的輸出目錄
    "esModuleInterop": true,    // 允許 default import CommonJS
    "strict": true
  },
  "include": ["src/**/*.ts"]
}
  • module:決定編譯後的模組系統。
  • moduleResolution:即使 moduleCommonJS,也常使用 node 解析規則,讓路徑解析行為與 Node 完全相同。
  • esModuleInterop:在使用 CommonJS 模組時,允許 import foo from "foo" 的寫法,避免 requiredefault 的衝突。

3. 常見模組選項說明與範例

3.1 CommonJS

適用於 Node.js(特別是版本 < 12)或是需要在測試環境直接 require 的情況。

// src/util.ts
export function add(a: number, b: number): number {
  return a + b;
}

// src/index.ts
import { add } from "./util";

console.log(add(2, 3));

編譯後(module: "CommonJS"):

// dist/util.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function add(a, b) {
    return a + b;
}
exports.add = add;

// dist/index.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("./util");
console.log((0, util_1.add)(2, 3));

技巧:若使用 esModuleInterop: true,可以改寫 import add from "./util",編譯器會自動插入 __importDefault 處理。


3.2 ES6 / ES2015

適合 現代瀏覽器 或是 Webpack / Rollup 等打包工具。這種設定會保留原生 import / export,讓工具在 tree‑shaking 時更有效。

// src/math.ts
export const PI = 3.14159;
export function area(r: number): number {
  return PI * r * r;
}

// src/app.ts
import { area } from "./math";

console.log(area(5));

編譯後(module: "ES6"):

// dist/math.js
export const PI = 3.14159;
export function area(r) {
  return PI * r * r;
}

// dist/app.js
import { area } from "./math.js";
console.log(area(5));

注意:Node 原生支援 ES 模組時,需要檔案副檔名為 .mjs 或在 package.json"type": "module"


3.3 ESNext

保留最新的模組特性(如 import.meta、動態 import),適合 使用 Vite、esbuild 等支援原生 ESNext 的工具。

// src/dynamic.ts
export async function loadModule(name: string) {
  const mod = await import(`./${name}`);
  return mod.default;
}

編譯後(module: "ESNext")仍保留 import(),讓打包器自行決定分割點(code‑splitting)。


3.4 UMD

適合 發布第三方函式庫,讓使用者可以在 AMD、CommonJS 或直接在 <script> 標籤中使用。

// src/lib.ts
export function greet(name: string) {
  return `Hello, ${name}!`;
}

編譯(module: "UMD"):

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.myLib = {}));
}(this, (function (exports) { 'use strict';
  function greet(name) {
    return `Hello, ${name}!`;
  }
  exports.greet = greet;
})));

實務:發布到 npm 時,常會同時提供 main(CommonJS)與 module(ESM)兩個入口,讓使用者自行選擇。


3.5 其他較少使用的選項

選項 產出說明 何時使用
AMD define([...], fn),配合 RequireJS 老舊前端專案
System System.register([...]),配合 SystemJS 動態載入需求
None 不產生模組語法,只保留檔案結構 只做型別檢查,交給外部工具(如 Babel)處理

4. 程式碼範例彙總

以下示範 同一段 TypeScript,在不同 module 設定下的編譯結果差異(簡化版):

// src/message.ts
export const hello = "Hello";
export function greet(name: string) {
  return `${hello}, ${name}!`;
}
module 設定 編譯結果(dist/message.js)
CommonJS exports.hello = "Hello"; function greet(name) { return "Hello, ".concat(name, "!"); } exports.greet = greet;
ES6 export const hello = "Hello"; export function greet(name) { return ${hello}, ${name}!; }
UMD 包裹在立即執行函式內,支援多種環境(如前表)
None 只會保留 import / export,不會產生任何 module 相關語法

常見陷阱與最佳實踐

陷阱 可能的錯誤訊息或行為 解決方案
moduletarget 不相容 module: "ESNext"target: "ES5",導致瀏覽器不支援 import target 至少提升到 ES2015,或改用 module: "CommonJS"
importrequire 混用 import foo from "foo"(CommonJS)產生 foo.default is undefined 開啟 esModuleInterop 或改寫為 import * as foo from "foo"
缺少檔案副檔名 import { foo } from "./foo" 在 ES 模組下產生 Cannot find module './foo' module: "ES6" 時,寫成 import { foo } from "./foo.js"(或使用 bundler 自動補足)
使用 module: "UMD" 時忘記設定 global 在瀏覽器直接 <script> 使用時 myLib 為 undefined 確認 umd: { name: "myLib" } 或在 package.jsonumd: "MyLib"
打包工具忽略 module Webpack 仍把 ES6 模組當成 CommonJS,導致 tree‑shaking 失效 確保 webpack.config.jsmodule.rules 使用 ts-loader / babel-loader 並保留 import,或在 tsconfig.jsonmodule: "ESNext"

最佳實踐

  1. 依目標環境選擇模組

    • Node.js(v12+) → module: "CommonJS"(舊版)或 module: "ESNext"(配合 "type": "module")。
    • 前端 SPA → module: "ESNext"(配合 Vite/webpack)。
    • 發布庫 → 同時提供 CommonJS (main) 與 ESM (module) 兩個入口。
  2. 保持 moduleResolution: "node"
    讓 TypeScript 依照 Node 的解析規則尋找 index.tspackage.json#exports 等,避免路徑錯誤。

  3. 開啟 esModuleInterop
    在混用 CommonJS 與 ES 模組時,能讓 import foo from "foo" 正常運作,減少 default 相關的錯誤。

  4. 使用 paths & baseUrl
    若專案使用別名(如 @utils/*),在 tsconfig.json 中設定對應的 paths,同時在 webpack/rollup 中同步設定別名,避免編譯與打包不一致。

  5. 檢查產出檔案大小
    使用 module: "ESNext" 搭配 sideEffects: false(在 package.json)可讓打包工具更有效地剔除未使用的程式碼。


實際應用場景

1. Node.js 後端服務(Express)

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "dist",
    "esModuleInterop": true,
    "strict": true
  }
}
  • 使用 CommonJSrequire('express') 正常工作。
  • esModuleInterop 允許 import express from "express",減少 import * as express 的冗長。

2. 前端 React 專案(Vite)

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true
  }
}
  • module: "ESNext" 讓 Vite 能直接保留原生 import,支援自動代碼分割(code‑splitting)。
  • moduleResolution: "bundler"(Vite 內建)與 moduleResolution: "node" 類似,但更貼合 Vite 的解析流程。

3. 開源函式庫(UMD + ESM)

// tsconfig.json (兩套設定)
{
  "compilerOptions": {
    "target": "ES5",
    "module": "UMD",
    "declaration": true,
    "outDir": "dist/umd"
  },
  "include": ["src/**/*"]
}
// tsconfig.esm.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "ESNext",
    "outDir": "dist/esm"
  }
}
  • UMD 產出 dist/umd/index.js,供 CDN 使用。
  • ESM 產出 dist/esm/index.js,供現代打包工具使用。
  • package.json 同時設定 "main": "./dist/umd/index.js""module": "./dist/esm/index.js",讓使用者自動選擇最適合的入口。

總結

compilerOptions.module 是 TypeScript 與 JavaScript 生態系統接軌的關鍵開關:

  • 決定產出語法(CommonJS、ES6、UMD…)
  • 影響相依性解析打包行為執行環境相容性
  • 配合 targetmoduleResolutionesModuleInterop 才能避免常見的「模組找不到」或「default 為 undefined」等錯誤

在實務開發中,只要依 目標環境(Node / 瀏覽器 / 第三方庫)選擇合適的 module,再搭配 最佳實踐(如 esModuleInterop、別名同步、提供多種入口),就能確保 TypeScript 編譯出的程式碼在任何情境下都能順利執行、保持可維護性,並充分利用現代打包工具的 tree‑shaking 與 code‑splitting 能力。

祝你在 TypeScript 的模組世界裡,寫出乾淨、可預測、且易於部署的程式碼! 🚀