本文 AI 產出,尚未審核

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. declareexport 的結合

模組模式--module esnext--module commonjs)下,我們常會在 .d.ts 中同時使用 declareexport,讓外部程式碼能以模組方式匯入:

// 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,並確保輸出目錄正確。

最佳實踐

  1. 集中管理型別宣告:在 src/types/ 目錄下建立 *.d.ts,讓專案結構清晰。
  2. 優先使用官方 @types:若有相對應的 DefinitelyTyped 套件,直接安裝 npm i -D @types/xxx,減少自行宣告的負擔。
  3. 搭配 export 使用:在模組環境中,declare + export 能讓其他檔案直接 import,提升可維護性。
  4. 保持型別同步:第三方程式庫升版後,檢查 .d.ts 是否仍符合實作,必要時更新或重新產生。
  5. 使用 /// <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 專案中,我們可能為 fshttp 等原生模組寫插件或 wrapper。使用 declare module "fs" 重新宣告擴充的函式,讓其他開發者在使用時得到完整的型別支援。

5. 多平台(React Native、Electron)共用程式碼

不同平台會提供不同的全域變數(如 navigatorprocess)。透過平台專屬的 declare,可以在同一套 TypeScript 程式碼中根據編譯目標切換型別,避免條件編譯時的型別衝突。


總結

  • declareTypeScript 與非 TypeScript 資源橋接的關鍵,讓我們在保持型別安全的同時,無縫使用全域變數、函式、類別與模組。
  • 正確的 ambient 宣告(全域、模組、命名空間)能提供完整的 IntelliSense,提升開發效率與程式碼可讀性。
  • 常見的錯誤多半源於 宣告位置、檔案類型與全域污染,只要遵循「分離型別、模組化、集中管理」的原則,就能避免大多數問題。
  • 在實務上,declare 的使用範圍廣泛:從整合 CDN、舊版腳本,到為 Web Worker、Node 原生模組寫型別擴充,都能看到它的身影。

掌握 declare,不僅能讓你的 TypeScript 專案 更安全、更易維護,也能在面對各式第三方資源時,保持開發的流暢與自信。祝你在 TypeScript 的世界裡寫出更健壯、更具可讀性的程式碼! 🚀