TypeScript – 型別宣告與整合
主題:declare 關鍵字
簡介
在大型前端專案或是與第三方程式庫整合時,我們常會碰到 「這個變數、函式或模組在 TypeScript 中找不到宣告」 的情況。
雖然程式在執行時仍能正常運作(因為它是由 JavaScript 實作),但 TypeScript 編譯器會因為缺乏型別資訊而報錯,導致開發體驗大打折扣。
declare 關鍵字正是為了解決這類問題而設計的。它允許我們 向編譯器聲明 某個已存在於執行環境的實體(變數、函式、類別、模組…),而不會產生任何 JavaScript 輸出。透過正確的 declare,我們可以:
- 取得 完整的型別檢查與 IntelliSense 支援
- 在 跨語言、跨執行環境(例如 Node、瀏覽器、WebWorker)間安全地使用外部資源
- 維持 純粹的 TypeScript 程式碼,避免手動寫冗長的
any型別
以下會一步步說明 declare 的概念、用法與實務技巧,讓你在專案中自信地整合任何第三方資源。
核心概念
1. declare 的基本語法
declare 只是一個 編譯期指示,不會在編譯結果中產生任何程式碼。它的結構與普通的宣告相似,只是多了 declare 前綴:
declare const GLOBAL_VAR: string;
declare function fetchData(url: string): Promise<any>;
declare class Logger {
log(message: string): void;
}
重點:
declare只能用在 全域或模組範圍,不能放在函式內部。
2. 宣告全域變數與函式
當我們在 HTML 中直接使用 <script src="https://cdn.jsdelivr.net/.../lodash.js"></script>,Lodash 會掛載在全域的 _ 物件上。若直接在 TypeScript 檔案裡使用 _,編譯器會提示「Cannot find name '_'」。
此時,我們可以透過 declare 為它建立型別:
// global.d.ts
declare const _: _.LoDashStatic;
// 使用範例
const numbers = [1, 2, 3];
const doubled = _.map(numbers, n => n * 2);
說明:
declare const _: _.LoDashStatic;告訴編譯器全域變數_的型別是_.LoDashStatic(需要自行或透過 @types 取得)。- 無需
import,因為_已在執行環境中存在。
3. 宣告外部模組(Ambient Modules)
有時候我們會使用 非 npm 發行的自製模組,例如把一段腳本直接放在 public/vendor/custom.js,然後在程式中 import "custom"。若沒有對應的型別定義,編譯器會報錯。
使用「ambient module」可以解決:
// custom.d.ts
declare module "custom" {
export function greet(name: string): string;
export const VERSION: number;
}
// app.ts
import { greet, VERSION } from "custom";
console.log(greet("TypeScript")); // => "Hello, TypeScript"
console.log(`v${VERSION}`);
說明:
declare module "custom"表示「在編譯期,模組名稱為custom的模組已經存在」。- 內部的
export只描述型別,實際實作仍由custom.js提供。
4. 宣告命名空間(Namespace)
舊版的 JavaScript 程式常使用全域命名空間(如 MyLib.Utils)。若要在 TypeScript 中使用,我們可以把它包在 declare namespace 裡:
// mylib.d.ts
declare namespace MyLib {
function init(config: { apiKey: string }): void;
namespace Utils {
function formatDate(date: Date, pattern?: string): string;
}
}
// 使用範例
MyLib.init({ apiKey: "12345" });
const today = MyLib.Utils.formatDate(new Date(), "YYYY/MM/DD");
5. declare 與 export 的結合
在 模組模式(--module esnext、--module commonjs)下,我們常會在 .d.ts 中同時使用 declare 與 export,讓外部程式碼能以模組方式匯入:
// api.d.ts
export interface User {
id: number;
name: string;
}
declare function getUser(id: number): Promise<User>;
export { getUser };
// client.ts
import { getUser, User } from "./api";
async function show() {
const u: User = await getUser(1);
console.log(u.name);
}
說明:
declare function getUser...只提供型別;export { getUser }讓它成為可匯出的模組成員。
程式碼範例彙整
以下列出 5 個實用範例,展示 declare 在不同情境的寫法與註解。
範例 1:宣告全域 window 擴充屬性
// global.d.ts
declare interface Window {
/** 自訂的全域設定 */
__APP_CONFIG__: {
apiBase: string;
debug: boolean;
};
}
// 在程式碼中直接使用
if (window.__APP_CONFIG__.debug) {
console.log("Debug mode is ON");
}
範例 2:宣告第三方 CDN 載入的 moment 函式
// moment.d.ts
declare const moment: typeof import("moment");
// 使用方式
const now = moment();
console.log(now.format("YYYY-MM-DD"));
範例 3:宣告自製的 worker.ts 內部訊息型別
// worker.d.ts
declare module "./worker" {
export interface WorkerMessage {
type: "log" | "error";
payload: string;
}
}
// 主執行緒
import type { WorkerMessage } from "./worker";
const w = new Worker(new URL("./worker.ts", import.meta.url));
w.onmessage = (e: MessageEvent<WorkerMessage>) => {
console.log(`[${e.data.type}] ${e.data.payload}`);
};
範例 4:宣告 Node.js 原生模組的擴充功能
// node-extensions.d.ts
declare module "fs" {
/** 讀取檔案並自動移除 BOM */
export function readFileTrimmed(path: string, encoding?: BufferEncoding): Promise<string>;
}
// 使用
import { readFileTrimmed } from "fs";
(async () => {
const txt = await readFileTrimmed("./data.txt", "utf8");
console.log(txt);
})();
範例 5:使用 declare const enum 提升編譯效能
// status.d.ts
declare const enum HttpStatus {
OK = 200,
NotFound = 404,
InternalError = 500,
}
// 在程式中
function handle(status: HttpStatus) {
if (status === HttpStatus.OK) {
console.log("成功");
}
}
提示:
const enum會在編譯階段直接內嵌數值,執行時不會產生額外的物件。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記放在 .d.ts 檔案 |
declare 若寫在普通的 .ts 檔案,編譯器仍會產生程式碼,可能導致重複定義或執行錯誤。 |
將純型別宣告放在 *.d.ts,確保不會被編譯成 JavaScript。 |
使用 any 逃避型別 |
為了快速解決錯誤,直接改成 any 失去型別安全。 |
盡量使用 declare 搭配正確的型別(介面、類別、泛型),保留 IntelliSense。 |
| 全域污染 | 隨意在全域宣告變數會與其他套件衝突。 | 優先使用 模組化 (declare module) 或 命名空間 (declare namespace) 包裹。 |
declare 與實作混用 |
在同一檔案內同時寫 declare 與實作程式碼會產生不一致的型別。 |
把實作與宣告分離:實作放在 .ts,型別放在對應的 .d.ts。 |
忽略 --declaration 設定 |
若未啟用 declaration,產生的 .d.ts 可能缺失,導致套件使用者無型別資訊。 |
在 tsconfig.json 中加入 "declaration": true,並確保輸出目錄正確。 |
最佳實踐:
- 集中管理型別宣告:在
src/types/目錄下建立*.d.ts,讓專案結構清晰。 - 優先使用官方 @types:若有相對應的 DefinitelyTyped 套件,直接安裝
npm i -D @types/xxx,減少自行宣告的負擔。 - 搭配
export使用:在模組環境中,declare+export能讓其他檔案直接import,提升可維護性。 - 保持型別同步:第三方程式庫升版後,檢查
.d.ts是否仍符合實作,必要時更新或重新產生。 - 使用
/// <reference types="..." />:在需要跨檔案引用時,加入 Triple‑Slash 指令,避免全域污染。
實際應用場景
1. 整合老舊 JavaScript 函式庫
許多公司仍在使用自行開發的老舊腳本(例如 legacy.js),而新專案則全面採用 TypeScript。只要寫一個 legacy.d.ts,描述全域函式與變數,即可在 TypeScript 中安全使用,且不必重寫整個庫。
2. 使用 CDN 或外部 API
在單頁應用(SPA)中,我們常透過 <script src="https://unpkg.com/axios/dist/axios.min.js"></script> 直接載入第三方庫。declare const axios: AxiosStatic; 讓 TypeScript 能正確推斷 axios 的型別,提供自動補完與錯誤檢查。
3. 建立自訂的 Web Worker 型別
Web Worker 的訊息傳遞是動態的,若不宣告型別,postMessage 會變成 any。透過 declare module "./worker",我們可以為 MessageEvent 指定具體的介面,減少執行時的錯誤。
4. Node.js 原生模組擴充
在 Node 專案中,我們可能為 fs、http 等原生模組寫插件或 wrapper。使用 declare module "fs" 重新宣告擴充的函式,讓其他開發者在使用時得到完整的型別支援。
5. 多平台(React Native、Electron)共用程式碼
不同平台會提供不同的全域變數(如 navigator、process)。透過平台專屬的 declare,可以在同一套 TypeScript 程式碼中根據編譯目標切換型別,避免條件編譯時的型別衝突。
總結
declare是 TypeScript 與非 TypeScript 資源橋接的關鍵,讓我們在保持型別安全的同時,無縫使用全域變數、函式、類別與模組。- 正確的 ambient 宣告(全域、模組、命名空間)能提供完整的 IntelliSense,提升開發效率與程式碼可讀性。
- 常見的錯誤多半源於 宣告位置、檔案類型與全域污染,只要遵循「分離型別、模組化、集中管理」的原則,就能避免大多數問題。
- 在實務上,
declare的使用範圍廣泛:從整合 CDN、舊版腳本,到為 Web Worker、Node 原生模組寫型別擴充,都能看到它的身影。
掌握 declare,不僅能讓你的 TypeScript 專案 更安全、更易維護,也能在面對各式第三方資源時,保持開發的流暢與自信。祝你在 TypeScript 的世界裡寫出更健壯、更具可讀性的程式碼! 🚀