本文 AI 產出,尚未審核

TypeScript – 模組與命名空間(Modules & Namespaces)

主題:ES Modules(import / export


簡介

在現代前端與 Node.js 開發中,模組化已成為必備的程式設計概念。它讓我們可以把程式切割成可重用、易維護的小單位,並透過明確的介面(export)與依賴(import)進行溝通。
ES Modules(簡稱 ESM)是 ECMAScript 官方標準的模組系統,已被所有主流瀏覽器與 Node.js 完全支援。TypeScript 完全遵循 ESM 的語法與行為,同時提供型別檢查與編譯期的安全性,讓開發者在寫 JavaScript 時也能享受到靜態型別的好處。

本篇文章將從 概念、語法、實作範例 三個層面,帶領讀者深入了解 ES Modules 在 TypeScript 中的使用方式,並分享常見陷阱與實務最佳實踐,幫助你在真實專案中快速上手、寫出更乾淨、更可維護的程式碼。


核心概念

1. ES Modules 的基本語法

關鍵字 用途 範例
export 將變數、函式、類別、型別等 公開 給其他模組使用 export const PI = 3.14;
export default 唯一 的預設匯出,讓匯入端可以自行命名 export default class Logger {}
import 從其他模組 匯入 先前 export 的成員 import { PI } from "./constants";
import * as 匯入整個模組為一個命名空間物件 import * as utils from "./utils";
import … from 匯入預設匯出 import Logger from "./logger";

:在 TypeScript 中,export 也可以直接匯出型別(typeinterface),但型別資訊只在編譯期存在,最終產出的 JavaScript 不會保留。


2. 匯出(Export)方式

2.1 命名匯出(Named Export)

// file: math.ts
export const add = (a: number, b: number): number => a + b;
export const mul = (a: number, b: number): number => a * b;

// 也可以一次匯出多個成員
export { sub, div };

function sub(a: number, b: number): number {
  return a - b;
}
function div(a: number, b: number): number {
  return a / b;
}

重點:命名匯出可以在同一檔案中出現多次,匯入端必須使用相同的名稱(或使用 as 重新命名)。

2.2 預設匯出(Default Export)

// file: logger.ts
export default class Logger {
  private prefix: string;
  constructor(prefix: string = "[Log]") {
    this.prefix = prefix;
  }
  info(msg: string) {
    console.log(`${this.prefix} ${msg}`);
  }
}

技巧:若模組只輸出單一功能或類別,使用 default 可以讓匯入程式碼更簡潔。

2.3 同時使用 Named 與 Default

// file: config.ts
export const API_URL = "https://api.example.com";
export const TIMEOUT = 5000;

export default {
  API_URL,
  TIMEOUT,
};

3. 匯入(Import)方式

3.1 匯入命名匯出

// file: app.ts
import { add, mul } from "./math";

console.log(add(2, 3)); // 5
console.log(mul(4, 5)); // 20

3.2 匯入預設匯出

// file: main.ts
import Logger from "./logger";

const log = new Logger("[MyApp]");
log.info("啟動完成");

3.3 匯入所有成員(Namespace Import)

// file: utilDemo.ts
import * as MathUtil from "./math";

console.log(MathUtil.add(1, 2)); // 3
console.log(MathUtil.mul(3, 4)); // 12

3.4 重新命名匯入

// file: renameDemo.ts
import { add as sum, mul as product } from "./math";

console.log(sum(5, 6)); // 11
console.log(product(2, 7)); // 14

3.5 動態匯入(Dynamic Import)

// file: lazyLoad.ts
async function loadLogger() {
  const { default: Logger } = await import("./logger");
  const logger = new Logger("[Lazy]");
  logger.info("動態匯入成功");
}
loadLogger();

說明import() 會回傳一個 Promise,適合在需要 懶載入(code‑splitting)或條件載入的情境。


4. TypeScript 與 ES Modules 的相容設定

設定項目 說明 範例
module 指定編譯目標的模組系統,ESNextES2020CommonJS "module": "ESNext"
target 設定編譯後的 JavaScript 版本 "target": "ES2019"
esModuleInterop 允許 import foo = require("foo")import foo from "foo" 混用 "esModuleInterop": true
allowSyntheticDefaultImports 允許在沒有 default 匯出的模組上使用預設匯入 "allowSyntheticDefaultImports": true
// tsconfig.json 範例
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "ESNext",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

程式碼範例(實務應用)

範例 1:建立一個簡易的 HTTP Client 模組

// file: httpClient.ts
export interface RequestOptions {
  method?: "GET" | "POST" | "PUT" | "DELETE";
  headers?: Record<string, string>;
  body?: any;
}

/**
 * 使用 fetch 包裝的通用 HTTP 請求函式
 * @param url 請求的 URL
 * @param options 可選的設定
 */
export async function request<T = any>(url: string, options: RequestOptions = {}): Promise<T> {
  const { method = "GET", headers = {}, body } = options;
  const response = await fetch(url, {
    method,
    headers: {
      "Content-Type": "application/json",
      ...headers,
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  return (await response.json()) as T;
}
// file: userService.ts
import { request, RequestOptions } from "./httpClient";

export interface User {
  id: number;
  name: string;
  email: string;
}

/**
 * 取得使用者清單
 */
export async function getUsers(): Promise<User[]> {
  return request<User[]>("/api/users");
}

/**
 * 新增使用者
 */
export async function createUser(user: Omit<User, "id">): Promise<User> {
  const opts: RequestOptions = { method: "POST", body: user };
  return request<User>("/api/users", opts);
}

重點:透過 型別參數 <T>request 能在呼叫端取得正確的回傳型別,減少手動斷言的錯誤機會。


範例 2:使用 預設匯出 建立一個 Logger 單例

// file: logger.ts
class Logger {
  private prefix: string;
  constructor(prefix = "[App]") {
    this.prefix = prefix;
  }
  log(message: string, ...optionalParams: any[]) {
    console.log(`${this.prefix} ${message}`, ...optionalParams);
  }
  error(message: string, ...optionalParams: any[]) {
    console.error(`${this.prefix} ${message}`, ...optionalParams);
  }
}

// 匯出唯一實例
const logger = new Logger("[MyProject]");
export default logger;
// file: index.ts
import logger from "./logger";

logger.log("系統啟動");
logger.error("發生未處理的例外");

實務技巧:使用 預設匯出 搭配單例模式,可避免在大型專案中重複建立相同工具物件。


範例 3:動態匯入 搭配 Tree Shaking

// file: chart.ts
export function drawBarChart(data: number[]) {
  console.log("畫 Bar Chart", data);
}
export function drawPieChart(data: number[]) {
  console.log("畫 Pie Chart", data);
}
// file: dashboard.ts
async function loadChart(type: "bar" | "pie") {
  const module = await import("./chart");
  if (type === "bar") {
    module.drawBarChart([10, 20, 30]);
  } else {
    module.drawPieChart([40, 60, 20]);
  }
}
loadChart("pie");

說明:Webpack、Vite 等打包工具會根據 import() 的路徑只載入實際使用的函式,減少最終 bundle 大小。


範例 4:混用 CommonJS 與 ES Modules(在 Node.js 中)

// file: legacy.js (CommonJS)
module.exports = {
  greet(name) {
    return `Hello, ${name}!`;
  },
};
// file: modern.ts (ESM)
import legacy from "./legacy.js"; // 需要 esModuleInterop / allowSyntheticDefaultImports

console.log(legacy.greet("TypeScript"));

提醒:在 Node.js 18+,若 package.json 設定 "type": "module",則 .js 會被視為 ES Module,需使用 .cjsrequire() 方式載入舊模組。


常見陷阱與最佳實踐

陷阱 可能的結果 解決方案 / 最佳實踐
忘記加 .js 副檔名(在 Node 的 ES Module) 執行時 ERR_MODULE_NOT_FOUND import 路徑中寫完整的檔名(./utils.js),或使用 tsc --moduleResolution node16
同時使用 export defaultexport,但匯入時只寫預設 其他成員無法被使用 匯入時 同時{ … }defaultimport logger, { level } from "./logger"
tsconfig.json 中未開啟 esModuleInterop,導致 import foo from "foo" 錯誤 必須使用 import * as foo from "foo",代碼較冗長 設定 "esModuleInterop": true 讓 CommonJS 模組也能使用預設匯入
動態匯入的路徑寫成相對路徑但缺少 .js(在編譯後) 打包後找不到模組 使用 import(/* webpackChunkName: "chart" */ "./chart.js") 或在 tsconfig 裡設定 moduleResolution: "node"
過度使用 export * from,導致命名衝突 編譯期或執行期報錯 明確列出要匯出的成員,或使用 as 重新命名衝突的項目

最佳實踐

  1. 盡量使用命名匯出:讓匯入端清楚知道每個成員的來源,減少未來重構時的破壞性變更。
  2. 預設匯出只用於單一核心功能(例如 React 元件、單例服務)。
  3. 保持檔案與資料夾結構的對應:每個功能模組放在自己的資料夾,並在 index.ts 中匯出公共介面,方便 import { Foo } from "@/features/foo"
  4. 使用 type 匯出純型別export type User = …,可避免在編譯後產生無效的 JavaScript。
  5. 設定嚴格的編譯選項"strict": true"noImplicitAny": true,確保模組之間的型別安全。

實際應用場景

場景 為何使用 ES Modules 範例
大型前端單頁應用(SPA) 透過 import/export 自然形成 依賴圖,配合 Webpack/Vite 進行 tree‑shakingcode splitting React + Redux:import store from "./store"export const actions = …
Node.js 微服務 Node 18+ 原生支援 ESM,讓服務間可以共享相同的工具函式庫,且不需要 Babel 轉譯。 import { connect } from "./db",在每個微服務中重複使用。
共用函式庫(npm package) 使用 export 再配合 package.json 中的 "exports" 欄位,限制外部只能存取公開 API。 export function formatDate(date: Date): string,在 package.json 設定 "exports": "./dist/index.js"
動態載入第三方套件 在使用者互動時才載入較大的套件(如圖表、編輯器),降低首次載入時間。 const { Editor } = await import("@toast-ui/editor");
SSR(Server‑Side Rendering) 使用同一套 ES Module 程式碼在伺服器與瀏覽器執行,避免寫兩套模組系統。 Next.js 中 import { getServerSideProps } from "./data"

總結

ES Modules 為 JavaScript(與 TypeScript)提供了 標準化、可預測 的模組機制。透過 export/import,我們可以:

  • 明確劃分 程式碼邊界,提升可讀性與維護性。
  • 利用編譯器與打包工具 進行型別檢查、tree‑shaking、code‑splitting,顯著改善效能。
  • 在前端與 Node.js 之間共享相同的模組寫法,降低學習成本與技術債。

在實務開發中,建議 以命名匯出為主,僅在單一核心功能時使用預設匯出;同時配合 tsconfig.json 的嚴格設定與適當的檔案結構,能讓你的 TypeScript 專案在長期維護上更加穩固。

掌握了 ES Modules 的概念與最佳實踐,你就能在任何規模的 TypeScript 專案中,寫出 乾淨、可測試且易於擴充 的程式碼。祝開發順利,持續享受模組化帶來的開發快感!