本文 AI 產出,尚未審核

TypeScript – 型別宣告與整合:自訂 .d.ts 型別檔


簡介

在大型前端或 Node.js 專案中,我們常會使用第三方函式庫、原生瀏覽器 API,甚至是自家寫的 JavaScript 模組。這些程式碼本身 沒有 TypeScript 的型別資訊,如果直接在 TypeScript 檔案中引用,編譯器會把它視為 any,失去靜態檢查與 IntelliSense 的好處,容易產生執行時錯誤。

自訂 .d.ts(宣告檔) 正是為了解決這個問題:它讓我們能夠在不改動原始 JavaScript 程式碼的前提下,為其補上完整的型別描述。透過宣告檔,開發者可以:

  • 讓編譯器正確推斷參數、回傳值與屬性型別
  • 在 IDE 中取得自動完成與文件提示
  • 以型別安全的方式整合多種程式庫,降低維護成本

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握自訂 .d.ts 的技巧,並提供幾個在實務專案中常見的應用場景。


核心概念

1. 為什麼要使用 .d.ts

  • 第三方 JavaScript 套件:許多套件尚未提供官方型別檔,或型別檔不完整。
  • 自家寫的純 JavaScript 模組:在遷移到 TypeScript 時,先寫宣告檔可以讓遷移過程平滑。
  • 全域變數或瀏覽器 API:有時會使用到非標準或實驗性的 API,需要自行宣告。

重點.d.ts 只會提供型別資訊,不會產生任何 JavaScript 程式碼。

2. 基本語法概覽

關鍵字 說明
declare 告訴編譯器「此處僅為型別宣告」;常用於函式、變數、模組等。
namespace 為全域變數或舊式模組建立命名空間,避免衝突。
export / export = 讓宣告檔支援 ES 模組或 CommonJS 匯出方式。
/// <reference path="..." /> 手動引用其他宣告檔(較少使用,因為 tsconfig.json 已能自動解析)。

3. 常見的宣告檔類型

類型 範例檔名 用途
全域 (Global) globals.d.ts 為全域變數、介面、函式提供型別。
模組 (Module) my-lib.d.ts import / require 的套件提供型別。
擴充 (Augmentation) vue-shim.d.ts 為現有套件或全域介面加入額外屬性。

程式碼範例

以下示範 5 個實務中常見的 .d.ts 撰寫方式,從最簡單的全域變數到完整的模組宣告皆有涵蓋。

範例 1️⃣:全域變數與函式

/* globals.d.ts */
declare const API_BASE_URL: string;

/**
 * 取得目前使用者的 JWT token
 * @returns token 字串,若未登入則回傳 null
 */
declare function getAuthToken(): string | null;

說明declare const 讓全域常數 API_BASE_URL 具備 string 型別;declare function 為全域函式提供參數與回傳型別。

範例 2️⃣:為純 JavaScript 模組寫模組宣告(CommonJS)

假設有一個 logger.js

// logger.js
module.exports = {
  log: (msg) => console.log(`[LOG] ${msg}`),
  error: (msg) => console.error(`[ERR] ${msg}`)
};

對應的 .d.ts

/* logger.d.ts */
declare module "logger" {
  export function log(msg: string): void;
  export function error(msg: string): void;
}

說明:使用 declare module "logger" 讓 TypeScript 能以 ES6 import 語法引用 logger,同時保留 CommonJS 的匯出結構。

範例 3️⃣:ES Module 宣告(預設匯出)

/* utils.d.ts */
declare module "utils" {
  export default function formatDate(date: Date, format?: string): string;
}

使用方式:

import formatDate from "utils";

const today = formatDate(new Date(), "YYYY/MM/DD");

重點export default 必須寫在 declare module 內,才能正確對應 ES6 預設匯出。

範例 4️⃣:擴充第三方套件(模組合併)

假設使用 express,想為 Request 介面加上自訂屬性 userId

/* express-augmentation.d.ts */
import "express";

declare module "express-serve-static-core" {
  interface Request {
    /** 目前登入使用者的 ID */
    userId?: string;
  }
}

說明:先 import "express" 讓 TypeScript 載入原始型別,接著使用 declare module 針對其子模組 express-serve-static-core 進行介面合併(augmentation),不會破壞原本的型別定義。

範例 5️⃣:宣告非標準瀏覽器 API(全域介面)

/* web-apis.d.ts */
interface Navigator {
  /** 判斷是否支援 WebXR */
  readonly xr?: any;
}

/**
 * 啟動 WebXR Session
 * @param mode Session 類型,預設為 'immersive-vr'
 */
declare function startXRSession(mode?: "immersive-vr" | "immersive-ar"): Promise<void>;

使用情境:在開發 VR/AR 網頁時,瀏覽器尚未正式支援 navigator.xr,我們自行宣告以取得型別提示。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記加 declare 直接寫 function foo(){} 會產生 JavaScript,導致編譯錯誤。 必須使用 declare function foo(...): ...
宣告檔路徑未被辨識 tsconfig.json 未包含 .d.ts 所在目錄。 tsconfig.jsoninclude 中加入 **/*.d.ts 或指定 typeRoots
重複宣告衝突 同一名稱在多個檔案中重複宣告,會產生「Duplicate identifier」錯誤。 利用 命名空間模組合併,或將宣告集中於同一檔案。
使用 any 過度 為了快速解決錯誤而把所有型別寫成 any,失去 TypeScript 的優勢。 盡量使用具體型別或 泛型,必要時才使用 unknown 再做類型守衛。
忘記匯出 宣告檔內的介面或型別未 export,外部無法引用。 在模組宣告裡使用 export interface …export type …

最佳實踐

  1. 分層管理:全域宣告放在 globals.d.ts,第三方套件放在 types/ 目錄,專案自訂模組放在 src/types/
  2. 保持同步:當原始 JavaScript 檔案變動時,立即更新對應的 .d.ts,避免型別不一致。
  3. 使用 --noEmit:在純型別檔案的專案中,設定 tsc --noEmit,只做型別檢查,避免產生不必要的 JS。
  4. 結合 JSDoc:在 .d.ts 中加入 JSDoc 註解,可在 IDE 中顯示說明,提升開發者體驗。
  5. 測試型別:利用 tsddtslint 這類工具撰寫型別測試,確保宣告檔的正確性。

實際應用場景

1. 專案遷移:從 JavaScript 到 TypeScript

情境:大型專案已有 10,000 行 JavaScript,直接改寫成本過高。
做法:先為每個已有模組寫對應的 .d.ts,讓新撰寫的 TypeScript 檔案可以安全引用。逐步將核心模組改寫為 TypeScript,同時保留原始 JavaScript。

2. 使用未提供型別的第三方套件

情境:使用 chart.js 的舊版套件,官方未提供型別。
解決:在 types/chartjs.d.ts 中宣告主要的構造函式與介面,讓整個專案在使用 new Chart(...) 時得到完整的型別提示。

3. 前端框架自訂插件

情境:在 Vue 3 中開發自訂指令 v-debounce,想讓 TypeScript 認識指令的參數型別。
做法:在 vue-shim.d.ts 中擴充 ComponentCustomProperties,加入 v-debounce 的型別描述,讓組件內使用指令時不會出現 any 警告。

4. 企業內部共用函式庫

情境:公司內部有一套共用的 api-client,以純 JavaScript 發布於私有 npm。
做法:在每個使用此套件的專案中加入 api-client.d.ts,或在私有 npm 中同時發布 @types/api-client,確保所有開發者都有一致的型別資訊。


總結

自訂 .d.ts 型別檔是 橋接 JavaScript 與 TypeScript 的關鍵工具。透過正確的宣告,我們可以:

  • 為第三方或自家 JavaScript 套件提供完整的型別資訊
  • 在 IDE 中獲得自動完成與文件提示,提高開發效率
  • 在大型或遷移專案中保持型別安全,減少執行時錯誤

本文從概念、語法、實作範例、常見陷阱與最佳實踐,以及實務應用場景逐層說明,期望讀者能在 1500 字左右的篇幅 中快速掌握自訂 .d.ts 的要領。只要遵循「分層管理、保持同步、加入文件註解」的原則,便能在日常開發中穩健地使用 TypeScript,讓程式碼更加可讀、可維護、且錯誤率更低。

最後提醒:型別宣告不是一次寫好就永遠不變的文件;隨著程式碼演進,持續更新 .d.ts 才能真正發揮 TypeScript 的力量。祝你在型別世界裡玩得開心!