本文 AI 產出,尚未審核

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

主題:全域宣告(global namespace)


簡介

在大型前端或 Node.js 專案中,全域變數往往是最容易產生衝突與維護困難的根源。
TypeScript 為了兼容既有的 JavaScript 生態,提供了 global(全域)宣告的機制,讓開發者可以在 全域命名空間 中擴充或重新定義介面、類別、函式等型別資訊,而不必改寫原有的模組結構。

掌握全域宣告的寫法與使用時機,能幫助你:

  1. 安全地 與第三方庫(如 jQuery、lodash)或瀏覽器原生 API 互動。
  2. 多檔案多套件 專案中避免型別衝突。
  3. 全域腳本(例如在 <script> 標籤直接載入的程式)提供完整的型別支援。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,一路帶你深入了解 TypeScript 的全域宣告,並提供實務上可直接套用的範例。


核心概念

1. 為什麼需要全域宣告?

在純 JavaScript 中,所有未使用 var/let/const 或未包在模組內的變數,都會掛到 window(瀏覽器)或 global(Node)物件上。
當我們在 TypeScript 中想要 使用或擴充 這些全域變數時,編譯器必須知道它們的型別資訊,否則會出現 any未定義 的錯誤。

舉例:直接在瀏覽器 console 輸入 $.ajax(...),若沒有對 $(jQuery)進行型別宣告,TypeScript 會報錯。

因此,我們需要透過 declare globaldeclare var 來告訴編譯器「這是一個全域變數,它的型別是…」。


2. declaredeclare global 的差別

關鍵字 用途 範例
declare var 宣告單一全域變數或函式,不會產生實際的 JavaScript 程式碼。 declare var myGlobal: string;
declare function 宣告全域函式。 declare function greet(name: string): void;
declare namespace 宣告全域命名空間(可內含多個型別)。 declare namespace MyLib { ... }
declare global 模組檔案export/import)中,擴充已存在的全域命名空間。 declare global { interface Window { foo: string; } }

注意declare global 必須寫在 模組檔案(即檔案內有 importexport)之中,否則會被視為普通的全域宣告。


3. 基本語法

// global.d.ts
declare var MY_GLOBAL_CONST: number;

declare function log(message: string): void;

declare namespace MyUtility {
  function format(date: Date): string;
  const version: string;
}

以上宣告只會在 型別檢查階段 生效,編譯後不會產生任何程式碼。


4. 在模組中擴充全域(declare global

假設我們有一個第三方函式庫 awesome-lib,它在全域掛載了一個 awesome 物件,但沒有提供 TypeScript 定義檔。可以這樣做:

// awesome-lib-augment.ts
import "awesome-lib"; // 這行讓檔案成為模組

declare global {
  interface Window {
    /** 代表 awesome-lib 在瀏覽器全域的入口點 */
    awesome: Awesome;
  }

  interface Awesome {
    /** 執行特殊功能 */
    doMagic(param: string): Promise<boolean>;
  }
}

// 之後就可以在任何檔案直接使用 window.awesome
export {};

重點:最後的 export {} 讓檔案保持為模組,避免 declare global 被視為普通的全域宣告。


5. 多檔案全域宣告的合併(Declaration Merging)

TypeScript 支援同名的 declare 會自動合併,這對於大型專案的分割非常有用。

// a.d.ts
declare namespace App {
  interface Config {
    apiUrl: string;
  }
}

// b.d.ts
declare namespace App {
  interface Config {
    timeout: number; // 合併到同一個 Config 介面
  }
}

最終 App.Config 會同時擁有 apiUrltimeout 兩個屬性。


程式碼範例

下面提供 5 個實用範例,從最簡單的全域變數宣告到在模組中擴充 Window,每段程式碼均附上說明。

範例 1:宣告全域常數與函式

// globals.d.ts
declare const APP_VERSION: string;          // 全域常數
declare function alertInfo(msg: string): void; // 全域函式

// 使用
console.log(`目前版本:${APP_VERSION}`);
alertInfo('系統已啟動');

說明declare const 只會在型別層面告訴編譯器 APP_VERSIONstring,實際值必須在其他腳本(如 <script>)中自行定義。


範例 2:全域命名空間(Namespace)與內部介面

// utils.d.ts
declare namespace Utils {
  /** 產生隨機字串 */
  function randomId(length: number): string;

  /** 日期格式化工具 */
  interface Formatter {
    (date: Date, format: string): string;
  }

  const formatDate: Formatter;
}

使用方式

const id = Utils.randomId(8);
const now = Utils.formatDate(new Date(), 'YYYY-MM-DD');
console.log(id, now);

範例 3:在模組中擴充 Window(declare global)

// analytics-augment.ts
import "./analytics-lib"; // 讓檔案成為模組

declare global {
  interface Window {
    /** 第三方分析 SDK */
    analytics: {
      track(event: string, data?: Record<string, any>): void;
    };
  }
}

// 直接使用
window.analytics.track('page_view', { url: location.href });
export {};

重點:若忘了 export {}declare global 會失效,導致 window.analytics 仍被視為 any


範例 4:合併多檔案的全域介面(Declaration Merging)

// user.d.ts
declare namespace App {
  interface User {
    id: number;
    name: string;
  }
}

// user-extend.d.ts
declare namespace App {
  interface User {
    email?: string;   // 可選屬性
    isAdmin: boolean;
  }
}

使用

const admin: App.User = {
  id: 1,
  name: 'Alice',
  isAdmin: true,
};

User 介面已同時擁有 email?isAdmin


範例 5:為第三方 JavaScript 庫(沒有 .d.ts)寫全域宣告

假設有一個舊版的 chart.js,在全域掛載 Chart 物件。

// chartjs-global.d.ts
declare class Chart {
  constructor(context: CanvasRenderingContext2D, config: any);
  update(): void;
  destroy(): void;
}

// 使用
const ctx = (document.getElementById('myChart') as HTMLCanvasElement).getContext('2d')!;
const myChart = new Chart(ctx, {
  type: 'bar',
  data: { /* ... */ },
});
myChart.update();

技巧:如果只需要部分功能,可只宣告需要的屬性/方法,減少維護成本。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記 export {} declare global 必須在模組內,否則不會生效。 在檔案最後加入 export {};,或直接使用 import/export
全域變數與模組變數同名 會產生 名稱衝突,導致型別不一致。 使用 命名空間前綴 來區分,例如 MyApp_Config
過度使用 any 為了快速寫程式,常把全域變數宣告為 any,失去型別安全。 儘量提供 精確的介面類別 定義。
宣告檔未被編譯器讀取 .d.ts 檔案位置不在 tsconfig.jsoninclude 範圍。 確認 tsconfig.json 中的 include 包含所有宣告檔,或使用 /// <reference path="..."/>
在瀏覽器與 Node 同時使用全域 windowglobal 不同,會造成錯誤。 使用 條件型別環境檢查typeof window !== 'undefined')。

最佳實踐

  1. 最小化全域:除非真的需要(例如第三方腳本),盡量在模組內使用 export/import,降低全域污染。
  2. 分層管理:將全域宣告集中在 src/typesglobal.d.ts,方便維護與搜尋。
  3. 使用 declare namespace:將相關的全域變數、函式包在同一個命名空間,避免名稱衝突。
  4. 搭配 tsconfig.json:設定 typeRootspaths,讓編譯器自動找尋自訂全域型別。
  5. 寫測試:對於自訂的全域介面,使用單元測試驗證其行為,確保未因型別變更破壞既有程式。

實際應用場景

場景 為什麼需要全域宣告 範例
整合舊有的 jQuery 插件 插件直接掛在 $jQuery 上,沒有 TypeScript 定義。 declare var $: JQueryStatic;
在 HTML 中直接載入第三方 SDK(如 Google Maps) SDK 只會在瀏覽器全域注入 google 物件。 declare namespace google.maps { class Map {...} }
共用的環境變數(如 process.env.NODE_ENV 在前端編譯時需要讀取環境變數,但不想把它寫成模組。 `declare const NODE_ENV: 'development'
自訂的全域事件中心 需要在多個模組間透過 window.eventBus 發佈/訂閱事件。 declare global { interface Window { eventBus: EventEmitter; } }
Hybrid App(Web + Native) 原生層會注入 bridge 物件到全域,前端需要型別支援。 declare const bridge: { call(method: string, args: any[]): Promise<any>; };

透過上述方式,我們可以在不改變第三方腳本的前提下,為 TypeScript 提供完整的型別資訊,讓 IDE 提示、編譯檢查與自動完成功能都能正常運作。


總結

全域宣告是 TypeScript 與傳統 JavaScript 生態橋接的重要工具。掌握 declaredeclare globaldeclare namespace 的寫法,能讓你:

  • 安全地 使用外部全域腳本或舊版庫。
  • 保持型別安全,避免 any 帶來的隱藏錯誤。
  • 組織化 全域型別,降低維護成本。

在實務開發中,建議遵循「盡量減少全域、必要時集中管理」的原則,並配合 tsconfig.json、單元測試與代碼審查,讓全域宣告成為提升專案品質的利器。祝你在 TypeScript 的模組與命名空間世界裡玩得開心、寫得更安全! 🚀