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 |
export、import (原生) |
現代瀏覽器、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:即使module為CommonJS,也常使用node解析規則,讓路徑解析行為與 Node 完全相同。esModuleInterop:在使用CommonJS模組時,允許import foo from "foo"的寫法,避免require與default的衝突。
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 相關語法 |
常見陷阱與最佳實踐
| 陷阱 | 可能的錯誤訊息或行為 | 解決方案 |
|---|---|---|
module 與 target 不相容 |
設 module: "ESNext" 但 target: "ES5",導致瀏覽器不支援 import |
把 target 至少提升到 ES2015,或改用 module: "CommonJS" |
import 與 require 混用 |
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.json 加 umd: "MyLib" |
打包工具忽略 module |
Webpack 仍把 ES6 模組當成 CommonJS,導致 tree‑shaking 失效 | 確保 webpack.config.js 中 module.rules 使用 ts-loader / babel-loader 並保留 import,或在 tsconfig.json 設 module: "ESNext" |
最佳實踐
依目標環境選擇模組
- Node.js(v12+) →
module: "CommonJS"(舊版)或module: "ESNext"(配合"type": "module")。 - 前端 SPA →
module: "ESNext"(配合 Vite/webpack)。 - 發布庫 → 同時提供
CommonJS(main) 與ESM(module) 兩個入口。
- Node.js(v12+) →
保持
moduleResolution: "node"
讓 TypeScript 依照 Node 的解析規則尋找index.ts、package.json#exports等,避免路徑錯誤。開啟
esModuleInterop
在混用 CommonJS 與 ES 模組時,能讓import foo from "foo"正常運作,減少default相關的錯誤。使用
paths&baseUrl
若專案使用別名(如@utils/*),在tsconfig.json中設定對應的paths,同時在 webpack/rollup 中同步設定別名,避免編譯與打包不一致。檢查產出檔案大小
使用module: "ESNext"搭配sideEffects: false(在package.json)可讓打包工具更有效地剔除未使用的程式碼。
實際應用場景
1. Node.js 後端服務(Express)
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"outDir": "dist",
"esModuleInterop": true,
"strict": true
}
}
- 使用
CommonJS讓require('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…)
- 影響相依性解析、打包行為與執行環境相容性
- 配合
target、moduleResolution、esModuleInterop才能避免常見的「模組找不到」或「default 為 undefined」等錯誤
在實務開發中,只要依 目標環境(Node / 瀏覽器 / 第三方庫)選擇合適的 module,再搭配 最佳實踐(如 esModuleInterop、別名同步、提供多種入口),就能確保 TypeScript 編譯出的程式碼在任何情境下都能順利執行、保持可維護性,並充分利用現代打包工具的 tree‑shaking 與 code‑splitting 能力。
祝你在 TypeScript 的模組世界裡,寫出乾淨、可預測、且易於部署的程式碼! 🚀