本文 AI 產出,尚未審核

TypeScript 模組與命名空間:深入了解 module augmentation(模組擴充)


簡介

在大型前端或 Node.js 專案中,我們常會使用第三方套件(如 expresslodashmoment)或是自行切分功能模組。模組擴充(module augmentation)是 TypeScript 提供的一項強大機制,允許開發者在不修改原始程式碼的前提下,為既有模組或全域宣告加入額外的型別、函式或屬性。

透過模組擴充,我們可以:

  • 為第三方函式庫補足缺失的型別定義(例如舊版套件沒有提供完整的型別)
  • 為自家共用模組加入「插件」式的功能,保持核心程式碼的乾淨與可維護
  • 在不同的程式碼基底中使用 宣告合併(declaration merging)來形成更彈性的 API

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者一步一步掌握模組擴充的使用方式,適用於 初學者到中級開發者


核心概念

1. 為什麼需要模組擴充?

在 TypeScript 中,模組的型別資訊通常來自 .d.ts 宣告檔或是直接在程式碼中寫好的 export。當我們想要 在外部加入新屬性 時(例如在 express.Request 上加上自訂的 user 欄位),直接修改原始檔案既不切實際,也會造成升級衝突。模組擴充提供了一個 「外掛」 的方式,讓我們在自己的檔案中「補齊」或「延伸」既有模組的型別。

2. 基本語法

模組擴充的核心是 declare module "module-name",在此區塊內可以:

  • 擴充介面(interface):加入新屬性或方法
  • 擴充類別(class):加入 prototype 方法(需要使用 declare global
  • 擴充命名空間(namespace):加入額外的函式或常數
  • 擴充函式(function):使用 module augmentation 為已有函式加入 overload

範例框架:

// my-augmentation.ts
import "original-module";   // 必須先 import 原始模組,使 TypeScript 知道它的存在

declare module "original-module" {
  // 在此處寫入想要擴充的型別
}

⚠️ 注意declare module 必須使用 字串字面量(module name),而不是變數或相對路徑。


3. 常見的擴充方式

以下分別示範四種實務上最常見的情境。

3.1 為第三方函式庫的介面加入自訂屬性(以 Express 為例)

// express-augmentation.ts
import "express";

declare module "express-serve-static-core" {
  // Express 的 Request 介面其實在 express-serve-static-core 中定義
  interface Request {
    /** 目前登入的使用者資訊,已在驗證 middleware 中寫入 */
    user?: {
      id: string;
      name: string;
      roles: string[];
    };
  }
}

說明

  1. import "express" 讓 TypeScript 載入原始型別。
  2. 使用 declare module "express-serve-static-core"(這是 Express 真正定義 Request 的模組)。
  3. interface Request 中加入 user? 屬性,之後所有 req 物件都會自動擁有此屬性,IDE 也會正確提示。

3.2 為第三方函式庫新增全域函式(以 Lodash 為例)

// lodash-augmentation.ts
import _ from "lodash";

declare module "lodash" {
  interface LoDashStatic {
    /** 取得陣列中唯一值的次序索引 */
    uniqIndices<T>(array: T[]): number[];
  }
}

// 實作實際的函式(必須在同一檔案或另一個 .ts檔裡)
_.uniqIndices = function <T>(array: T[]): number[] {
  const seen = new Map<T, number>();
  const indices: number[] = [];

  array.forEach((item, idx) => {
    if (!seen.has(item)) {
      seen.set(item, idx);
      indices.push(idx);
    }
  });

  return indices;
};

說明

  • LoDashStatic 為 lodash 的全域物件類型,我們在裡面加上 uniqIndices 方法的型別宣告。
  • 接著直接把實作掛到 _ 上,讓程式碼在執行時能呼叫 _.uniqIndices([...])

3.3 為自訂模組加入「插件」功能(以 utils 為例)

假設有一個共用工具模組 utils.ts

// utils.ts
export function formatDate(date: Date, fmt: string): string {
  // 簡易實作...
  return "";
}

現在想在別的檔案中為 utils 加入 parseDate

// utils-augmentation.ts
import * as utils from "./utils";

declare module "./utils" {
  export function parseDate(str: string, fmt: string): Date;
}

// 實作
export function parseDate(str: string, fmt: string): Date {
  // 依照 fmt 解析日期字串
  return new Date(str);
}

說明

  • declare module "./utils" 必須使用相對路徑(相對於目前檔案)。
  • 在擴充區塊中 重新 export 新增的函式。
  • 實作部分可以直接寫在同一檔或分離,只要確保檔案被編譯即可。

3.4 為全域型別(global)加入宣告(以 Node.js 為例)

有時候我們需要在所有檔案裡都能使用自訂的全域變數:

// global-augmentation.d.ts
export {};

declare global {
  /** 應用程式的全域設定 */
  const APP_CONFIG: {
    env: "development" | "production";
    version: string;
  };
}

在任何 .ts 檔案中即可直接使用:

console.log(`Running ${APP_CONFIG.version} in ${APP_CONFIG.env}`);

說明

  • 必須先 export {}; 讓檔案被視為模組,才能使用 declare global
  • 這樣的寫法相當於把變數掛到 globalThis,但在編譯階段會得到完整的型別提示。

4. 模組擴充與宣告合併(Declaration Merging)

TypeScript 的 宣告合併 允許同名的 interfacenamespaceenum 等在不同檔案中自動合併。模組擴充正是利用這個機制:

// a.ts
export interface Settings {
  theme: "light" | "dark";
}

// b.ts
declare module "./a" {
  interface Settings {
    language: string;   // 合併至 Settings
  }
}

最終 Settings 會同時擁有 themelanguage 兩個屬性。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記先 import 原始模組 若未先 import "module",編譯器找不到原始型別,擴充會失敗。 在檔案頂部 import(即使不使用任何匯出),確保模組已被載入。
模組名稱寫錯 declare module 必須使用正確的 字串字面量(例如 "express-serve-static-core"),寫成相對路徑會找不到。 查閱套件的 .d.ts 檔或官方文件,確認實際的模組名稱。
擴充的型別與原始型別不相容 例如在介面中加入必填屬性,會導致原本的實作無法符合新型別。 使用可選屬性 (?)新增方法(不改變原有結構)。
重複宣告造成衝突 多個檔案同時擴充同一屬性,可能產生「重複定義」錯誤。 統一管理擴充檔案,或使用 namespace 內部的子介面分層。
未將擴充檔案加入 tsconfig.json 若檔案不在 includefiles 範圍內,編譯時不會看到擴充。 確認 tsconfig.json 中的 include 包含所有 *.d.ts*.ts 的擴充檔案。

最佳實踐

  1. 集中管理:將所有擴充放在 src/types/typings/ 目錄,統一 index.d.ts 入口。
  2. 保持擴充為可選:盡量使用 ? 或 overload,避免破壞原有程式碼的相容性。
  3. 寫測試:對於新增的介面屬性或函式,寫單元測試確保在執行時不會出現 undefined
  4. 文件化:在專案 README 或 Wiki 中說明哪些套件被擴充、擴充的目的與使用方式,方便新成員快速上手。

實際應用場景

場景 為什麼適合使用模組擴充 範例
Express 中的驗證資訊 多個路由都需要存取 req.user,而官方 Request 沒有此屬性。 參考 3.1express-augmentation.ts
為第三方 UI 套件加入自訂主題屬性 UI 套件如 antd 提供 ConfigProvider,但公司有自訂的 theme 設定。 declare module "antd/lib/config-provider",在 ConfigProviderProps 加入 customTheme?: ThemeConfig
在 lodash 加入專案專屬工具 團隊常用的 uniqIndices 在 lodash 中不存在,卻想保持統一的工具呼叫方式。 參考 3.2 的 lodash 擴充。
Node.js 全域環境變數 為了避免在每個檔案都 import config,直接掛在全域。 參考 3.4global-augmentation.d.ts
插件式的功能擴充 大型 SaaS 平台需要支援第三方插件,每個插件會在核心模組上掛載額外方法。 透過 declare module "./core" 為核心模組加入 registerPlugin(plugin: Plugin)

總結

  • module augmentation 是 TypeScript 提供的「外掛」機制,讓我們能在不改動原始程式碼的情況下,為既有模組或全域環境加入型別、函式與屬性。
  • 基本語法是 declare module "module-name",配合 importinterface 合併global 宣告 等技巧即可完成。
  • 實務上常見於 Express request 擴充第三方函式庫功能增補全域設定、以及 插件式架構
  • 使用時要避免忘記 import、模組名稱錯誤、以及不相容的必填屬性;同時遵守 集中管理、可選屬性、寫測試與文件化 的最佳實踐,可讓專案維護成本大幅降低。

掌握了模組擴充,你就能在大型 TypeScript 專案中,像拼圖般靈活地為任何模組加上自訂功能,提升程式碼的可讀性與可維護性。祝開發順利,玩得開心! 🎉