本文 AI 產出,尚未審核

TypeScript – 型別宣告與整合(Type Declarations & Integration)

主題:全域增強(Global Augmentation)


簡介

在大型前端或 Node.js 專案中,我們常會使用第三方函式庫、原生 API,或是自行撰寫的全域工具函式。這些程式碼往往 不在同一個模組,卻需要在 TypeScript 中取得正確的型別資訊。若直接使用 declare varinterface 等方式在局部檔案內宣告,會造成型別重複或無法在其他檔案存取的問題。

全域增強(global augmentation) 正是為了讓開發者能在不改動原始庫檔案的前提下,擴充或補充全域變數、介面、模組的型別。透過 declare globalglobalThis、或是「模組擴充」的技巧,我們可以:

  • 為第三方套件加入缺少的型別定義
  • 為全域函式或物件(例如 windowprocess)掛上自訂屬性
  • 在不同檔案間共享相同的型別宣告,避免重複定義

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者一步步掌握 global augmentation 的使用方式,並提供幾個在真實專案中常見的應用情境。


核心概念

1. 為什麼需要全域增強?

  • 第三方套件型別不完整
    某些套件只提供 JavaScript 原始碼,或僅有部分型別檔案,導致 TypeScript 編譯時出現 any 或錯誤訊息。
  • 自訂全域屬性
    例如在 window 上掛上 appConfig、在 process.env 中加入自訂環境變數,若不宣告型別,IDE 會失去自動完成與型別檢查的優勢。
  • 避免全域污染
    直接在全域作用域寫 interface Foo {} 會影響所有檔案,使用 模組增強(module augmentation)可以限定影響範圍,同時保留型別安全。

2. 基本語法:declare global

在一個 模組檔案(即檔案頂端有 importexport)中,我們可以使用 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)

有時候我們想只針對特定模組(例如 expresslodash)加入型別,而不是全域。這時可以使用 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 允許同名的 interfacenamespace 在不同檔案中合併。這對於全域增強非常實用,只要每個檔案都使用 declare globaldeclare 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 moduleisEven 加入 _.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.jsoninclude 包含 .d.ts 檔案,或在入口檔案 import './types/...';
重複宣告同名介面 (例如兩個檔案都宣告 interface Window) 會自動合併,但若屬性衝突會出現錯誤 使用 宣告合併 時,確保屬性名稱唯一;若需要區分,可使用 namespace 包裹
在 Node 中使用 window 編譯錯誤或執行時 ReferenceError 只在瀏覽器環境下增強 Window,Node 環境使用 globalThisglobal
忘記導入增強檔案 (尤其在多個入口點) 部分模組得不到增強,導致型別不一致 在每個入口點(如 src/main.tssrc/worker.ts)都 import './types/...';,或在 tsconfig.json 中使用 files 明確引用

最佳實踐

  1. 集中管理增強檔案
    建議在 src/types/ 目錄下建立專屬的 .d.ts 檔案,並在 src/index.ts(或其他入口)一次性匯入,確保全域增強在整個應用程式啟動前完成。

  2. 使用 declare module 時保留原始模組的型別
    增強只需要補足缺少的屬性,避免重新定義整個介面,以免失去官方提供的型別補助。

  3. 保留註解與 JSDoc
    為增強的屬性加上說明,讓 IDE 能顯示提示,提升團隊協作效率。

  4. tsconfig.json 設定 typeRoots
    若自訂型別檔案較多,將它們放在專屬資料夾,並在 compilerOptions.typeRoots 中加入路徑,避免與第三方 @types 產生衝突。

  5. 盡量避免在全域掛載大量功能
    全域變數過多會降低程式的可測試性與可維護性。若功能較複雜,考慮改用 依賴注入(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 augmentationexpress.Request 增加 user 屬性
第三方 UI 套件(如 Ant Design) 想在 ConfigProvider 中加入自訂主題屬性 透過 declare module 'antd' 增強 ConfigProviderProps
Lodash 自訂函式 團隊共用的工具函式希望保持在 _ 命名空間下 使用 declare module 'lodash'_.mixin 結合實作與型別增強

總結

  • 全域增強 為 TypeScript 提供了一套安全且彈性的方式,讓開發者可以在不修改第三方程式碼的情況下,為全域變數、介面或模組補足型別。
  • 透過 declare globaldeclare moduleglobalThis,我們可以分別在 全域作用域特定模組跨環境 中增強型別,從而提升 IDE 的自動完成、編譯時的型別檢查與程式的可讀性。
  • 在實作時,必須確保檔案是模組增強檔案被編譯器載入,並遵守 單一職責適度使用全域 的原則,才能避免常見的陷阱。
  • 本文提供的 5 個範例(window 設定、process.env、global logger、lodash 擴充、Express Request)涵蓋了前端、後端與第三方套件的常見需求,讀者可依照專案需求自行調整與擴充。

掌握 global augmentation,不僅能讓 TypeScript 的型別系統更完整,也能在大型專案中維持代碼的可維護性與可擴充性。祝你在 TypeScript 的世界裡寫出更安全、更高效的程式碼!