本文 AI 產出,尚未審核

TypeScript 模組與命名空間:declare 關鍵字深入解析


簡介

在大型前端或 Node.js 專案中,我們常會遇到 第三方函式庫全域變數非 TypeScript 撰寫的程式碼。這些資源本身並沒有提供 TypeScript 型別資訊,若直接在程式中使用,編譯器會抱怨「找不到名稱」或「類型為 any」。
declare 關鍵字正是為了解決這類問題而設計的:它讓開發者 宣告(declare)一個已存在於執行環境中的變數、函式或模組,而不會產生任何實際的 JavaScript 輸出。

本篇文章將以 模組與命名空間(Modules & Namespaces) 為背景,說明 declare 的語法、使用時機、常見陷阱與最佳實踐,並提供多個實務範例,幫助你在 TypeScript 專案中安全、有效地整合外部資源。


核心概念

1. declare 的基本語法

用法 說明
declare var foo: string; 宣告全域變數 foo,型別為 string,不會產生任何程式碼。
declare function bar(x: number): void; 宣告全域函式 bar,提供型別資訊。
declare const PI: number; 宣告常數,常用於全域常數或第三方庫的只讀屬性。
declare namespace MyLib { ... } 在命名空間內宣告多個成員,適合舊式的全域腳本庫。
declare module "lodash" { ... } 為外部模組提供型別定義,常見於沒有官方 .d.ts 的套件。

重點declare 僅在 型別層面 生效,編譯後不會產生任何 JavaScript 代碼。


2. declareexport / import 的配合

在 ES 模組(ESM)或 CommonJS 模組中,我們通常會使用 import 取得外部函式庫。若該模組缺少型別定義檔(.d.ts),可以自行建立 宣告檔*.d.ts):

// lodash.d.ts
declare module "lodash" {
  export function chunk<T>(array: T[], size?: number): T[][];
  // ... 其他你需要的型別
}

在程式碼中:

import { chunk } from "lodash";

const result = chunk([1, 2, 3, 4, 5], 2);
console.log(result); // [[1,2],[3,4],[5]]

此時 declare 只負責告訴編譯器「lodash 模組裡有 chunk 這個函式」,實際的執行仍由 Node.js 或瀏覽器的模組解析機制完成。


3. declare namespace 用於全域腳本

對於舊式的全域腳本(例如 jQueryGoogle Maps API),常見的做法是使用 命名空間

// jquery.d.ts
declare namespace $ {
  function ajax(url: string, settings?: any): XMLHttpRequest;
  // 其他 jQuery 方法...
}

使用時直接呼叫全域 $

$.ajax("https://api.example.com/data", {
  method: "GET",
  success: (data) => console.log(data),
});

提示:若你同時在專案中使用 ES 模組,建議把全域宣告放在 global.d.ts,並確保 tsconfig.jsoninclude 包含該檔案。


4. declare const enum 與編譯時優化

enum 在編譯後會產生對應的物件,若僅需要 編譯時的常數,可以使用 declare const enum

declare const enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

使用時:

function move(dir: Direction) {
  // 編譯後會直接寫成數字 1、2、3、4,沒有 enum 物件的跑位成本
}

注意declare const enum 必須在 宣告檔 中使用,因為它不會產生任何 JavaScript。


5. 多個宣告檔的合併(Declaration Merging)

TypeScript 允許同名的 declare 內容自動合併,這對於擴充第三方庫非常有用:

// lodash.d.ts (原始)
declare module "lodash" {
  export function chunk<T>(array: T[], size?: number): T[][];
}

// lodash-extend.d.ts (自訂擴充)
declare module "lodash" {
  export function camelCase(str: string): string;
}

編譯器會把兩個 declare module "lodash" 合併成一個完整的型別描述,讓你可以逐步擴充而不必一次寫完所有型別。


程式碼範例

以下提供 5 個實用範例,展示 declare 在不同情境下的最佳寫法。

範例 1:宣告全域變數(環境變數)

// env.d.ts
declare const process: {
  env: {
    NODE_ENV: "development" | "production" | "test";
    API_URL: string;
  };
};
// app.ts
if (process.env.NODE_ENV === "development") {
  console.log("開發模式,使用測試 API:", process.env.API_URL);
}

說明process.env 在 Node.js 中已存在,但 TypeScript 並不自動知道型別,透過 declare 提供精確的字串聯合類型。


範例 2:為沒有型別的第三方模組建宣告檔

// my-lib.d.ts
declare module "my-lib" {
  export interface Options {
    timeout?: number;
    verbose?: boolean;
  }
  export function init(config: Options): void;
  export const VERSION: string;
}
// main.ts
import { init, VERSION } from "my-lib";

init({ timeout: 3000, verbose: true });
console.log("MyLib 版本:", VERSION);

說明:只需要宣告你實際會使用的 API,減少維護成本。


範例 3:使用 declare namespace 包裝全域函式庫

// google-maps.d.ts
declare namespace google.maps {
  class Map {
    constructor(el: HTMLElement, opts: MapOptions);
    setZoom(level: number): void;
    // ...
  }
  interface MapOptions {
    center: LatLng;
    zoom: number;
  }
  class LatLng {
    constructor(lat: number, lng: number);
  }
}
// map.ts
const mapDiv = document.getElementById("map") as HTMLElement;
const map = new google.maps.Map(mapDiv, {
  center: new google.maps.LatLng(25.033964, 121.564472),
  zoom: 12,
});

說明:透過 declare namespace,讓全域的 google.maps 物件在 TypeScript 中擁有完整的型別提示。


範例 4:declare const enum 的編譯時優化

// http-status.d.ts
declare const enum HttpStatus {
  OK = 200,
  NotFound = 404,
  InternalError = 500,
}
// api.ts
function handleResponse(status: number) {
  switch (status) {
    case HttpStatus.OK:
      console.log("成功");
      break;
    case HttpStatus.NotFound:
      console.log("找不到資源");
      break;
  }
}

編譯結果(api.js):

function handleResponse(status) {
  switch (status) {
    case 200:
      console.log("成功");
      break;
    case 404:
      console.log("找不到資源");
      break;
  }
}

說明HttpStatus 在執行時根本不存在,減少了額外的物件查找。


範例 5:宣告全域函式(第三方 CDN 載入)

假設在 HTML 中透過 CDN 載入 moment.js

<script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/min/moment.min.js"></script>

在 TypeScript 中:

// moment-global.d.ts
declare const moment: {
  (input?: string | number | Date): Moment;
  utc(input?: string | number | Date): Moment;
  // 只列出你需要的部份即可
};

interface Moment {
  format(fmt?: string): string;
  // 其他常用方法...
}
// time.ts
const now = moment().format("YYYY-MM-DD HH:mm:ss");
console.log("現在時間:", now);

說明:即使 moment 是全域變數,透過 declare const 仍能取得完整的型別支援。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記把 .d.ts 加入 tsconfig.json 編譯器找不到宣告檔會仍報錯。 確認 includefiles 包含所有宣告檔,或將檔案放在 src 目錄下。
在宣告檔中寫入實作代碼 declare 檔案只能有型別,若出現實作會被編譯為 JavaScript,造成重複定義。 僅保留型別與 declare,實作應放在 .ts 檔。
使用 any 逃避型別檢查 雖然能快速通過編譯,但失去 TypeScript 的好處。 盡量提供具體型別,必要時可使用 unknown + 型別斷言。
過度宣告全域變數 全域污染會導致名稱衝突,且難以維護。 優先使用模組化 (import/export);若必須全域,使用唯一的命名空間前綴。
忘記 export declare module 中未導出成員,外部無法取得。 明確寫 export,或使用 export =(CommonJS)配合 import = require()

最佳實踐

  1. 只在宣告檔中使用 declare:保持 .d.ts 與實作檔分離,讓編譯器清晰辨識。
  2. 逐步擴充:利用 Declaration Merging,在不同檔案中分別補足第三方庫的型別。
  3. 使用 readonlyconst enum:提升執行效能與型別安全。
  4. 統一放置宣告檔:建議在專案根目錄建立 types/@types/ 資料夾,並在 tsconfig.json 中設定 "typeRoots": ["./types", "./node_modules/@types"]
  5. 結合 tsc --noEmit:在 CI 流程中檢查宣告檔是否正確,避免因型別遺失而產生執行時錯誤。

實際應用場景

場景 為何需要 declare 範例
使用舊版第三方腳本(如 jQuery) 這類腳本直接掛在全域,沒有模組系統。 declare const $: JQueryStatic;
Node.js 環境變數 process.env 需要明確的字串聯合類型,避免寫錯變數名稱。 declare const process: { env: { ... } };
從 CDN 載入的庫 無法透過 npm 安裝型別,必須手動寫宣告。 declare const moment: MomentStatic;
自訂的全域函式(如 analytics) 團隊共用的全域追蹤函式,需在多個檔案中呼叫。 declare function trackEvent(name: string, data?: any): void;
混合使用 ES 模組與 CommonJS 某些套件僅支援 module.exports,但專案使用 import declare module "legacy-lib" { export = LegacyLib; }

總結

  • declare型別宣告 的關鍵字,讓 TypeScript 能夠辨識已存在於執行環境的變數、函式、模組或命名空間,而不會產生額外的 JavaScript。
  • 模組化 的專案中,我們常透過 .d.ts 檔為缺少型別的第三方套件或全域腳本提供型別資訊;在 命名空間 中則以 declare namespace 包裝全域 API。
  • 正確使用 declare 能提升 IDE 補完、編譯時錯誤偵測與程式碼可讀性;同時,避免過度使用 any、保持檔案分離、善用 Declaration Merging,是維持大型專案健康的關鍵。
  • 透過上述範例與最佳實踐,你可以在日常開發中快速為外部資源建立安全的型別橋樑,讓 TypeScript 的靜態檢查真正發揮威力。

祝你在 TypeScript 的世界裡寫出更安全、更易維護的程式碼! 🚀