TypeScript – 型別宣告與整合(Type Declarations & Integration)
主題:全域增強(Global Augmentation)
簡介
在大型前端或 Node.js 專案中,我們常會使用第三方函式庫、原生 API,或是自行撰寫的全域工具函式。這些程式碼往往 不在同一個模組,卻需要在 TypeScript 中取得正確的型別資訊。若直接使用 declare var、interface 等方式在局部檔案內宣告,會造成型別重複或無法在其他檔案存取的問題。
全域增強(global augmentation) 正是為了讓開發者能在不改動原始庫檔案的前提下,擴充或補充全域變數、介面、模組的型別。透過 declare global、globalThis、或是「模組擴充」的技巧,我們可以:
- 為第三方套件加入缺少的型別定義
- 為全域函式或物件(例如
window、process)掛上自訂屬性 - 在不同檔案間共享相同的型別宣告,避免重複定義
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者一步步掌握 global augmentation 的使用方式,並提供幾個在真實專案中常見的應用情境。
核心概念
1. 為什麼需要全域增強?
- 第三方套件型別不完整
某些套件只提供 JavaScript 原始碼,或僅有部分型別檔案,導致 TypeScript 編譯時出現any或錯誤訊息。 - 自訂全域屬性
例如在window上掛上appConfig、在process.env中加入自訂環境變數,若不宣告型別,IDE 會失去自動完成與型別檢查的優勢。 - 避免全域污染
直接在全域作用域寫interface Foo {}會影響所有檔案,使用 模組增強(module augmentation)可以限定影響範圍,同時保留型別安全。
2. 基本語法:declare global
在一個 模組檔案(即檔案頂端有 import 或 export)中,我們可以使用 declare global 來宣告全域型別。範例:
// utils/global.d.ts
export {};
declare global {
interface Window {
/** 自訂的全域設定物件 */
__APP_CONFIG__: {
apiBaseUrl: string;
enableDebug: boolean;
};
}
/** 為 Node.js 的 process.env 增加自訂變數 */
namespace NodeJS {
interface ProcessEnv {
/** 只在開發環境使用的 API 金鑰 */
DEV_API_KEY?: string;
}
}
}
重點:檔案必須是 模組(
export {}或任何import),否則declare global會被視為全域區塊,無法正確合併。
3. 使用 globalThis 直接掛載屬性
globalThis 是 ECMAScript 2020 引入的統一全域物件,適用於瀏覽器、Node、Web Worker 等環境。若只需要在程式執行階段加入屬性,可配合型別宣告:
// global.ts
declare global {
interface GlobalThis {
/** 供整個應用程式共用的 logger */
logger: {
log: (msg: string) => void;
error: (msg: string) => void;
};
}
}
// 實作
globalThis.logger = {
log: (msg) => console.log(`[LOG] ${msg}`),
error: (msg) => console.error(`[ERR] ${msg}`),
};
// 其他檔案直接使用
logger.log('程式啟動完成');
注意:在 Node 中若使用
global,仍可透過globalThis取得相同效果,且在 TypeScript 中不需要額外的declare var global: typeof globalThis;。
4. 模組增強(Module Augmentation)
有時候我們想只針對特定模組(例如 express、lodash)加入型別,而不是全域。這時可以使用 module augmentation:
// express-augmentation.d.ts
import 'express';
declare module 'express' {
/** 為 Request 介面加入自訂屬性 */
interface Request {
/** 已驗證的使用者資訊 */
user?: {
id: string;
role: 'admin' | 'member';
};
}
}
使用時:
// server.ts
import express from 'express';
import './express-augmentation'; // 只要匯入一次即可生效
const app = express();
app.use((req, res, next) => {
// 假設已完成驗證,將使用者資訊掛在 req.user 上
req.user = { id: 'U123', role: 'admin' };
next();
});
app.get('/profile', (req, res) => {
// TypeScript 會正確推斷 req.user 的型別
if (req.user?.role === 'admin') {
res.send(`Hello Admin ${req.user.id}`);
} else {
res.status(403).send('Forbidden');
}
});
5. 多檔案增強的合併(Declaration Merging)
TypeScript 允許同名的 interface、namespace 在不同檔案中合併。這對於全域增強非常實用,只要每個檔案都使用 declare global 或 declare module,最終會形成單一的型別。
// a.d.ts
declare global {
interface MyGlobal {
foo: string;
}
}
// b.d.ts
declare global {
interface MyGlobal {
bar: number;
}
}
// 使用
const obj: MyGlobal = { foo: 'hello', bar: 123 };
程式碼範例
以下提供 5 個實用範例,從最簡單的全域變數到進階的第三方套件增強,逐步展示如何在專案中應用。
範例 1:在 window 上掛載自訂設定
// src/types/window.d.ts
export {};
declare global {
interface Window {
/** 應用程式的全域設定 */
__APP_CONFIG__: {
apiBaseUrl: string;
theme: 'light' | 'dark';
};
}
}
// src/config.ts
window.__APP_CONFIG__ = {
apiBaseUrl: 'https://api.example.com',
theme: 'light',
};
// src/util.ts
export function getApiUrl(path: string): string {
return `${window.__APP_CONFIG__.apiBaseUrl}/${path}`;
}
說明:
window.__APP_CONFIG__只需要在入口檔案設定一次,其他模組皆可安全取得型別。
範例 2:為 Node.js process.env 增加自訂變數
// src/types/env.d.ts
export {};
declare global {
namespace NodeJS {
interface ProcessEnv {
/** 只能在測試環境使用的旗標 */
TEST_MODE?: 'true' | 'false';
}
}
}
// src/index.ts
if (process.env.TEST_MODE === 'true') {
console.log('Running in test mode');
}
說明:在
tsconfig.json中加入"typeRoots": ["./src/types", "./node_modules/@types"],讓 TypeScript 能找到自訂型別檔。
範例 3:使用 globalThis 實作全域 logger
// src/logger.ts
declare global {
interface GlobalThis {
logger: {
info: (msg: string) => void;
warn: (msg: string) => void;
error: (msg: string) => void;
};
}
}
globalThis.logger = {
info: (msg) => console.info(`[INFO] ${msg}`),
warn: (msg) => console.warn(`[WARN] ${msg}`),
error: (msg) => console.error(`[ERROR] ${msg}`),
};
export default globalThis.logger;
// src/app.ts
import logger from './logger';
logger.info('應用程式啟動');
說明:所有檔案只要匯入
logger,即可在任何地方直接使用logger.xxx,且 IDE 會提供完整的型別提示。
範例 4:增強第三方套件 lodash 的自訂函式
假設我們在專案中自行擴充 lodash,加入 isEven 函式:
// src/types/lodash.d.ts
import 'lodash';
declare module 'lodash' {
interface LoDashStatic {
/** 判斷一個數字是否為偶數 */
isEven(num: number): boolean;
}
}
// src/lodash-extend.ts
import _ from 'lodash';
// 實作自訂函式
_.mixin({
isEven: (num: number) => num % 2 === 0,
});
export default _;
// src/main.ts
import _ from './lodash-extend';
if (_.isEven(42)) {
console.log('42 是偶數');
}
說明:透過
declare module把isEven加入_.isEven,不需要改動原始lodash程式碼,且型別安全。
範例 5:Express Request 介面的增強(實務常見)
// src/types/express.d.ts
import 'express';
declare module 'express' {
interface Request {
/** JWT 解碼後的使用者資訊 */
user?: {
id: string;
email: string;
roles: string[];
};
}
}
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import './types/express'; // 確保增強檔案被載入
export function authMiddleware(req: Request, _res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as any;
req.user = { id: payload.sub, email: payload.email, roles: payload.roles };
} catch {
// token 無效,保持 req.user 為 undefined
}
}
next();
}
// src/routes/profile.ts
import { Router, Request, Response } from 'express';
import './middleware/auth';
const router = Router();
router.get('/me', (req: Request, res: Response) => {
if (!req.user) {
return res.status(401).json({ message: '未授權' });
}
res.json({ id: req.user.id, email: req.user.email });
});
export default router;
說明:透過全域增強,
req.user在所有路由檔案中皆可正確取得型別,減少any或手動型別斷言。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方案 / 最佳做法 |
|---|---|---|
檔案不是模組 (declare global 放在沒有 import/export 的檔案) |
TypeScript 會把宣告視為全域,導致重複或無法合併 | 在增強檔案最上方加 export {};,或直接 import 任何內容,使其成為模組 |
| 增強檔案未被編譯器載入 | 型別不會生效,IDE 顯示錯誤 | 確保 tsconfig.json 的 include 包含 .d.ts 檔案,或在入口檔案 import './types/...'; |
重複宣告同名介面 (例如兩個檔案都宣告 interface Window) |
會自動合併,但若屬性衝突會出現錯誤 | 使用 宣告合併 時,確保屬性名稱唯一;若需要區分,可使用 namespace 包裹 |
在 Node 中使用 window |
編譯錯誤或執行時 ReferenceError |
只在瀏覽器環境下增強 Window,Node 環境使用 globalThis 或 global |
| 忘記導入增強檔案 (尤其在多個入口點) | 部分模組得不到增強,導致型別不一致 | 在每個入口點(如 src/main.ts、src/worker.ts)都 import './types/...';,或在 tsconfig.json 中使用 files 明確引用 |
最佳實踐
集中管理增強檔案
建議在src/types/目錄下建立專屬的.d.ts檔案,並在src/index.ts(或其他入口)一次性匯入,確保全域增強在整個應用程式啟動前完成。使用
declare module時保留原始模組的型別
增強只需要補足缺少的屬性,避免重新定義整個介面,以免失去官方提供的型別補助。保留註解與 JSDoc
為增強的屬性加上說明,讓 IDE 能顯示提示,提升團隊協作效率。在
tsconfig.json設定typeRoots
若自訂型別檔案較多,將它們放在專屬資料夾,並在compilerOptions.typeRoots中加入路徑,避免與第三方@types產生衝突。盡量避免在全域掛載大量功能
全域變數過多會降低程式的可測試性與可維護性。若功能較複雜,考慮改用 依賴注入(DI)或 單例服務,只在必要時使用全域增強。
實際應用場景
| 場景 | 為何需要全域增強 | 具體做法 |
|---|---|---|
| 多頁面 SPA(如 React + Vite) | 需要在所有頁面共享 window.__APP_CONFIG__ 或 globalThis.logger |
在 src/global.d.ts 中宣告,於 main.tsx 初始化一次 |
| Node.js 微服務 | 各服務共用環境變數與自訂 process.env 屬性 |
在 src/types/env.d.ts 中增強 ProcessEnv,於 src/server.ts 中讀取 |
| Express + JWT | 在每個路由都要存取已驗證的 req.user |
使用 module augmentation 為 express.Request 增加 user 屬性 |
| 第三方 UI 套件(如 Ant Design) | 想在 ConfigProvider 中加入自訂主題屬性 |
透過 declare module 'antd' 增強 ConfigProviderProps |
| Lodash 自訂函式 | 團隊共用的工具函式希望保持在 _ 命名空間下 |
使用 declare module 'lodash' 與 _.mixin 結合實作與型別增強 |
總結
- 全域增強 為 TypeScript 提供了一套安全且彈性的方式,讓開發者可以在不修改第三方程式碼的情況下,為全域變數、介面或模組補足型別。
- 透過
declare global、declare module、globalThis,我們可以分別在 全域作用域、特定模組、跨環境 中增強型別,從而提升 IDE 的自動完成、編譯時的型別檢查與程式的可讀性。 - 在實作時,必須確保檔案是模組、增強檔案被編譯器載入,並遵守 單一職責、適度使用全域 的原則,才能避免常見的陷阱。
- 本文提供的 5 個範例(window 設定、process.env、global logger、lodash 擴充、Express Request)涵蓋了前端、後端與第三方套件的常見需求,讀者可依照專案需求自行調整與擴充。
掌握 global augmentation,不僅能讓 TypeScript 的型別系統更完整,也能在大型專案中維持代碼的可維護性與可擴充性。祝你在 TypeScript 的世界裡寫出更安全、更高效的程式碼!