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. declare 與 export / 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 用於全域腳本
對於舊式的全域腳本(例如 jQuery、Google 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.json的include包含該檔案。
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 |
編譯器找不到宣告檔會仍報錯。 | 確認 include 或 files 包含所有宣告檔,或將檔案放在 src 目錄下。 |
| 在宣告檔中寫入實作代碼 | declare 檔案只能有型別,若出現實作會被編譯為 JavaScript,造成重複定義。 |
僅保留型別與 declare,實作應放在 .ts 檔。 |
使用 any 逃避型別檢查 |
雖然能快速通過編譯,但失去 TypeScript 的好處。 | 盡量提供具體型別,必要時可使用 unknown + 型別斷言。 |
| 過度宣告全域變數 | 全域污染會導致名稱衝突,且難以維護。 | 優先使用模組化 (import/export);若必須全域,使用唯一的命名空間前綴。 |
忘記 export |
在 declare module 中未導出成員,外部無法取得。 |
明確寫 export,或使用 export =(CommonJS)配合 import = require()。 |
最佳實踐
- 只在宣告檔中使用
declare:保持.d.ts與實作檔分離,讓編譯器清晰辨識。 - 逐步擴充:利用 Declaration Merging,在不同檔案中分別補足第三方庫的型別。
- 使用
readonly、const enum:提升執行效能與型別安全。 - 統一放置宣告檔:建議在專案根目錄建立
types/或@types/資料夾,並在tsconfig.json中設定"typeRoots": ["./types", "./node_modules/@types"]。 - 結合
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 的世界裡寫出更安全、更易維護的程式碼! 🚀