本文 AI 產出,尚未審核

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

主題:命名空間(namespace)


簡介

在大型的 TypeScript 專案中,程式碼的組織與可維護性 是成功的關鍵。
早期的 JavaScript 只能靠全域變數或立即執行函式 (IIFE) 來避免命名衝突,而 TypeScript 則提供了兩種官方的封裝機制:模組(module)命名空間(namespace)

本篇聚焦於 命名空間,說明它的設計目的、使用方式以及在什麼情況下仍值得採用。即使在 ES6+ 時代,了解 namespace 的概念仍能幫助開發者在遺留系統、腳本工具或是需要「單檔」部署的情境下,寫出結構清晰、避免全域污染的程式碼。


核心概念

1. 命名空間的基本語法

namespace MyLibrary {
  export const version = "1.0.0";

  export function greet(name: string): string {
    return `Hello, ${name}!`;
  }

  // 內部不會被外部直接存取的私有成員
  const secret = "only inside";
}
  • namespace 關鍵字宣告一個 命名空間,其內容會被包在同一個 JavaScript 立即函式中,避免污染全域。
  • 必須使用 export 標記想要公開的類別、函式、介面或變數,否則它只在命名空間內部可見。

2. 多檔案合併(Declaration Merging)

命名空間支援「宣告合併」:同名的 namespace 可以分散在多個檔案,只要在編譯時一起編譯,就會自動合併成一個完整的命名空間。

// file: math.ts
namespace Utilities {
  export function add(a: number, b: number): number {
    return a + b;
  }
}

// file: string.ts
namespace Utilities {
  export function capitalize(s: string): string {
    return s.charAt(0).toUpperCase() + s.slice(1);
  }
}

編譯 tsc math.ts string.ts --outFile utilities.js 後,Utilities 會同時擁有 addcapitalize 兩個方法。


3. 巢狀命名空間(Nested Namespaces)

為了更細緻的層級劃分,可以在命名空間內再宣告子命名空間:

namespace App {
  export namespace Models {
    export interface User {
      id: number;
      name: string;
    }
  }

  export namespace Services {
    export class UserService {
      getUser(id: number): App.Models.User {
        // 假設從伺服器取得資料
        return { id, name: "Alice" };
      }
    }
  }
}

使用時:

const svc = new App.Services.UserService();
const user = svc.getUser(1);
console.log(user.name); // Alice

4. 匯入外部 JavaScript(declare namespace)

當需要在 TypeScript 中使用已有的全域 JavaScript 函式庫(例如 jQuery、Google Maps),可以使用 declare namespace 只描述型別,而不產生任何 JavaScript:

declare namespace GoogleMaps {
  function initMap(mapId: string): void;
  interface LatLng {
    lat: number;
    lng: number;
  }
}

此時,編譯器只會檢查呼叫方式是否正確,而不會產生 GoogleMaps 的實作。


5. 與 ES 模組的互操作

雖然 ES6 模組是現在的主流,但有時候仍會在同一專案中同時使用 import/exportnamespace。常見做法是:

// utils.ts – ES 模組
export function formatDate(d: Date): string {
  return d.toISOString().split("T")[0];
}

// legacy.ts – 命名空間(舊式腳本)
namespace Legacy {
  export function log(msg: string): void {
    console.log("[Legacy] " + msg);
  }
}

在另一個檔案中混合使用:

import { formatDate } from "./utils";

Legacy.log(`今天日期:${formatDate(new Date())}`);

只要 tsconfig.jsonmodule 設為 esnextcommonjs,編譯器會自行處理兩者的相容性。


常見陷阱與最佳實踐

陷阱 說明 解決方式
全域污染 忘了 export,導致變數仍在命名空間外可見,破壞封裝。 始終使用 export,或在命名空間最外層加上 export 讓整體可見。
過度巢狀 命名空間層級太深,導致引用時寫出長長的路徑。 適度分層,只在功能上有明顯界線時才建立子命名空間。
與模組混用不當 同一檔案同時使用 import/exportnamespace,可能產生編譯警告。 盡量分檔:模組檔案使用 ES 模組,純腳本檔案使用 namespace。
宣告合併衝突 不同檔案中同名的介面或類別定義不一致,編譯器會報錯。 保持一致的 API 設計,或使用 declare module 取代 namespace。
編譯輸出設定 使用 --outFile 合併多個 namespace 時,若 module 設為 esnext 會失效。 module 設為 none(或 amd/system),或改用模組系統。

最佳實踐

  1. 僅在需要單檔輸出或與舊版腳本整合時使用 namespace。新專案優先選擇 ES 模組。
  2. 將公開 API 放在最外層命名空間,內部實作以 private 變數或未 export 的函式隱藏。
  3. 利用宣告合併 讓大型程式庫的型別維護更方便,但務必在 tsconfig.json 中設定正確的檔案包含範圍。
  4. 在 TypeScript 4.5+,考慮使用 export typeexport const enum 來減少編譯後的冗餘程式碼。

實際應用場景

1. 老舊前端專案的漸進式升級

一個使用 jQuery、Bootstrap、全域變數的舊系統,想要逐步引入 TypeScript。可以先把每個功能模組寫成 namespace,然後在 tsconfig.json 設定 outFile: bundle.js,最終仍以單一 <script> 載入,避免改動既有 HTML 結構。

2. 命令列工具(CLI)一次性打包

Node.js 的 CLI 工具往往只需要一個可執行檔。將所有指令實作放在同一個 namespace,配合 tsc --outFile cli.js,即可產生單檔且不依賴模組載入器的執行檔。

#!/usr/bin/env node
namespace CLI {
  export function run(argv: string[]) {
    if (argv.includes("--help")) {
      console.log("使用方式: mytool [options]");
    } else {
      console.log("執行中...");
    }
  }
}
CLI.run(process.argv.slice(2));

3. 宣告第三方全域庫的型別

許多外部腳本(例如 Google Analytics)直接掛在 window 上,沒有模組化的入口。使用 declare namespace 可以在 TypeScript 中安全地呼叫:

declare namespace ga {
  function create(trackingId: string, options?: object): void;
  function send(eventCategory: string, eventAction: string): void;
}
ga.create("UA-XXXXX-Y");
ga.send("button", "click");

總結

  • 命名空間 是 TypeScript 提供的「封裝」機制,適合單檔輸出、遺留系統或需要宣告全域變數的情境。
  • 透過 export、巢狀、宣告合併 等特性,我們可以在不污染全域的前提下,建立可維護的程式碼結構。
  • 在現代開發中,模組(module) 才是主流;但了解 namespace 的運作原理與最佳實踐,能讓我們更靈活地處理舊有專案或特殊需求。

掌握了命名空間的概念後,你就能在 TypeScript 生態中自如切換「模組」與「全域」兩種風格,根據專案需求選擇最合適的封裝方式,寫出既安全又易於維護的程式碼。祝你在 TypeScript 的旅程中,玩得開心、寫得順手!