TypeScript 模組與命名空間(Modules & Namespaces)
主題:全域宣告(global namespace)
簡介
在大型前端或 Node.js 專案中,全域變數往往是最容易產生衝突與維護困難的根源。
TypeScript 為了兼容既有的 JavaScript 生態,提供了 global(全域)宣告的機制,讓開發者可以在 全域命名空間 中擴充或重新定義介面、類別、函式等型別資訊,而不必改寫原有的模組結構。
掌握全域宣告的寫法與使用時機,能幫助你:
- 安全地 與第三方庫(如 jQuery、lodash)或瀏覽器原生 API 互動。
- 在 多檔案、多套件 專案中避免型別衝突。
- 為 全域腳本(例如在
<script>標籤直接載入的程式)提供完整的型別支援。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,一路帶你深入了解 TypeScript 的全域宣告,並提供實務上可直接套用的範例。
核心概念
1. 為什麼需要全域宣告?
在純 JavaScript 中,所有未使用 var/let/const 或未包在模組內的變數,都會掛到 window(瀏覽器)或 global(Node)物件上。
當我們在 TypeScript 中想要 使用或擴充 這些全域變數時,編譯器必須知道它們的型別資訊,否則會出現 any 或 未定義 的錯誤。
舉例:直接在瀏覽器 console 輸入
$.ajax(...),若沒有對$(jQuery)進行型別宣告,TypeScript 會報錯。
因此,我們需要透過 declare global 或 declare var 來告訴編譯器「這是一個全域變數,它的型別是…」。
2. declare 與 declare 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必須寫在 模組檔案(即檔案內有import或export)之中,否則會被視為普通的全域宣告。
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 會同時擁有 apiUrl 與 timeout 兩個屬性。
程式碼範例
下面提供 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_VERSION為string,實際值必須在其他腳本(如<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.json 的 include 範圍。 |
確認 tsconfig.json 中的 include 包含所有宣告檔,或使用 /// <reference path="..."/>。 |
| 在瀏覽器與 Node 同時使用全域 | window 與 global 不同,會造成錯誤。 |
使用 條件型別 或 環境檢查(typeof window !== 'undefined')。 |
最佳實踐
- 最小化全域:除非真的需要(例如第三方腳本),盡量在模組內使用
export/import,降低全域污染。 - 分層管理:將全域宣告集中在
src/types或global.d.ts,方便維護與搜尋。 - 使用
declare namespace:將相關的全域變數、函式包在同一個命名空間,避免名稱衝突。 - 搭配
tsconfig.json:設定typeRoots或paths,讓編譯器自動找尋自訂全域型別。 - 寫測試:對於自訂的全域介面,使用單元測試驗證其行為,確保未因型別變更破壞既有程式。
實際應用場景
| 場景 | 為什麼需要全域宣告 | 範例 |
|---|---|---|
| 整合舊有的 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 生態橋接的重要工具。掌握 declare、declare global、declare namespace 的寫法,能讓你:
- 安全地 使用外部全域腳本或舊版庫。
- 保持型別安全,避免
any帶來的隱藏錯誤。 - 組織化 全域型別,降低維護成本。
在實務開發中,建議遵循「盡量減少全域、必要時集中管理」的原則,並配合 tsconfig.json、單元測試與代碼審查,讓全域宣告成為提升專案品質的利器。祝你在 TypeScript 的模組與命名空間世界裡玩得開心、寫得更安全! 🚀