本文 AI 產出,尚未審核

TypeScript 與 JavaScript 整合(JS Interop)

主題:JSDoc 型別標註


簡介

在現代前端開發中,TypeScript 已成為提升程式碼可讀性與可靠性的首選語言。許多既有的 JavaScript 專案或第三方函式庫仍以純 JavaScript 撰寫,若直接在 TypeScript 專案中使用,往往會失去型別資訊,導致編譯器無法提供完整的錯誤檢查與自動完成。

JSDoc 是一套在 JavaScript 程式碼中加入註解的標準,透過在註解中寫入型別描述,TypeScript 編譯器可以「讀取」這些資訊,進而在 JS Interop(JavaScript 與 TypeScript 互操作) 時提供完整的型別推斷與檢查。本文將說明 JSDoc 型別標註的核心概念、實作範例、常見陷阱與最佳實踐,協助你在混合專案中安全、有效地使用 TypeScript。


核心概念

1. 為什麼要使用 JSDoc?

  • 不改變原始碼結構:不需要把整個檔案改寫成 .ts,只要加上註解即可。
  • 即時型別提示:IDE(如 VS Code)會根據 JSDoc 提供自動完成、跳轉與錯誤提示。
  • 兼容第三方庫:對於沒有官方 TypeScript 定義檔(.d.ts)的套件,JSDoc 是快速補救的方式。

Tip:若專案已全面遷移至 TypeScript,仍可保留 JSDoc 作為文件說明,提升可讀性。

2. 基本語法

在 JavaScript 檔案(.js)最上方或函式前加入 /** ... */ 註解,使用 @param@returns@type 等標籤描述型別。例如:

/**
 * 計算兩個數字的總和
 * @param {number} a 第一個加數
 * @param {number} b 第二個加數
 * @returns {number} 兩數相加的結果
 */
function add(a, b) {
  return a + b;
}

上述註解讓 TypeScript 看到 add 的簽名是 (a: number, b: number) => number,即使檔案本身是純 JavaScript。

3. 常見的 JSDoc 標籤

標籤 用途 範例
@param {type} name description 描述函式參數 @param {string} name 使用者名稱
@returns {type} description 描述回傳值 @returns {Promise<void>} 完成的 Promise
@type {type} 為變數或屬性指定型別 /** @type {Array<string>} */ const list = []
@typedef {type} Name 定義自訂型別 /** @typedef {{id:number, name:string}} User */
@property {type} name description @typedef 中描述屬性 @property {number} id 使用者編號
@enum {type} 定義列舉 /** @enum {string} */ const Color = { Red: "red", Blue: "blue" }
@template T 泛型(在 TypeScript 中對應 <T> /** @template T @param {T[]} arr */

程式碼範例

以下示範 5 個在實務中常見且實用的 JSDoc 型別標註範例,並說明每段註解的意義。

範例 1:簡單函式與預設參數

/**
 * 產生歡迎訊息
 * @param {string} [name="訪客"] 使用者名稱,預設為「訪客」
 * @returns {string} 完整的歡迎文字
 */
function greet(name = "訪客") {
  return `哈囉,${name}!`;
}
  • [] 表示參數是可選的,且可同時設定預設值。
  • TypeScript 會推斷 greet 的型別為 (name?: string) => string

範例 2:物件解構與型別別名

/**
 * @typedef {Object} User
 * @property {number} id 使用者編號
 * @property {string} name 使用者名稱
 * @property {string[]} [roles] 使用者角色陣列(可選)
 */

/**
 * 取得使用者的主要角色
 * @param {User} user 使用者物件
 * @returns {string} 第一個角色,若無則回傳「guest」
 */
function getPrimaryRole({ roles = [] }) {
  return roles[0] || "guest";
}
  • 透過 @typedef 建立 User 型別,讓多處使用時保持一致。
  • 解構參數時,JSDoc 仍能正確對 user 進行型別檢查。

範例 3:Promise 與 async/await

/**
 * 從遠端取得商品資料
 * @param {number} productId 商品編號
 * @returns {Promise<{ id: number; name: string; price: number }>} 商品資訊的 Promise
 */
async function fetchProduct(productId) {
  const response = await fetch(`/api/products/${productId}`);
  const data = await response.json();
  return data; // data 會被視為符合上述型別
}
  • @returns {Promise<...>} 告訴編譯器此函式回傳 Promise,內部的型別也一併描述。
  • 在使用端,IDE 能自動提示 data.iddata.name 等屬性。

範例 4:泛型(Template)與陣列操作

/**
 * 依據條件過濾陣列
 * @template T
 * @param {T[]} items 原始陣列
 * @param {(item: T) => boolean} predicate 判斷函式
 * @returns {T[]} 符合條件的子陣列
 */
function filter(items, predicate) {
  const result = [];
  for (const item of items) {
    if (predicate(item)) result.push(item);
  }
  return result;
}

// 使用範例
/** @type {number[]} */
const numbers = [1, 2, 3, 4];
const evens = filter(numbers, n => n % 2 === 0); // evens 被推斷為 number[]
  • @template T 等同於 TypeScript 的 <T>,讓函式支援任意型別的陣列。
  • 透過 JSDocfilter 的回傳型別會自動與輸入陣列保持一致。

範例 5:類別與私有屬性(使用 # 前綴)

/**
 * 表示一個簡易的計數器
 */
class Counter {
  /** @type {number} */
  #count = 0; // 私有屬性,TS 會視為 number

  /**
   * 取得目前的計數值
   * @returns {number}
   */
  get value() {
    return this.#count;
  }

  /**
   * 增加計數
   * @param {number} [step=1] 增量,預設為 1
   */
  increment(step = 1) {
    this.#count += step;
  }
}
  • #count私有欄位,在 JSDoc 中仍可使用 @type 標註。
  • 類別成員的型別資訊會自動被 TypeScript 讀取,讓外部程式碼在使用 Counter 時得到正確提示。

常見陷阱與最佳實踐

陷阱 說明 建議的解決方式
註解錯誤或遺漏 @param@returns 與實際程式碼不符,TypeScript 仍會以註解為主,可能產生誤導。 保持註解與程式同步;使用 IDE 的「檢查 JSDoc」功能自動提醒。
過度使用 any /** @type {any} */ 會讓型別檢查失效,失去 TypeScript 的好處。 盡量避免 any,改用具體型別或 unknown,必要時再加上類型斷言。
未啟用 checkJs TypeScript 預設不檢查 .js 檔案的 JSDoc。 tsconfig.json 中設定 "checkJs": true,並確保 "allowJs": true
混用 CommonJS 與 ES Module JSDoc 只描述型別,不會解決模組解析問題。 統一專案的模組系統,或在 tsconfig.json 中正確設定 moduleesModuleInterop
忽略 @typedef 的作用域 @typedef 若寫在函式內部,只在該函式可見。 將共用型別放在檔案最上方或獨立的 .d.ts,確保全域可見。

最佳實踐

  1. 逐步遷移:先在新開發的檔案加入 JSDoc,逐漸覆蓋舊有程式碼。
  2. 使用 /** @ts-ignore */ 只在必要時暫時忽略型別錯誤,避免濫用。
  3. 配合 lint 工具:如 eslint-plugin-jsdoc 可自動檢查 JSDoc 格式與完整性。
  4. 文件即程式碼:將 JSDoc 視為 API 文件,保持與實作同步,提升團隊協作效率。

實際應用場景

  1. 引入第三方 JavaScript 套件
    某些套件(如舊版的 lodashmoment)沒有官方的 .d.ts,可自行在 node_modules 旁建立 *.js 檔案,加入 JSDoc 描述,讓 TypeScript 編譯器即時提供型別資訊。

  2. 遺留系統的漸進式升級
    大型企業常有多年累積的 JavaScript 程式碼,直接改寫成本過高。透過 JSDoc,開發者可在不改變執行行為的前提下,逐步為關鍵模組加上型別。

  3. 前端框架的自訂指令或插件
    在 Vue、React 等框架中,開發自訂指令或高階元件時,使用 JSDoc 為 props、context、hook 回傳值標註型別,可讓使用者在 IDE 中得到即時提示,降低使用錯誤。

  4. Node.js 後端服務
    Express、Koa 等輕量框架多以 JavaScript 撰寫,配合 JSDoc 為 req.bodyres.locals 等屬性標註,可在撰寫路由處理函式時得到完整的型別支援。


總結

  • JSDoc 為 JavaScript 提供了一條低侵入、易上手的型別描述管道,讓 TypeScript 能在混合專案中發揮完整的型別檢查與 IDE 支援。
  • 掌握 基本標籤@param@returns@type)與 進階技巧@typedef@template@enum),即可在日常開發中快速為函式、物件、類別加上型別。
  • 識別常見陷阱、遵守最佳實踐,能避免註解與程式碼不同步、過度使用 any 等問題,確保型別資訊的可靠性。
  • 第三方套件、遺留系統、框架插件與 Node.js 後端 等實務情境中,JSDoc 是將 JavaScript 平滑過渡到 TypeScript 的關鍵橋樑。

透過本文的概念與範例,你已具備在任何 JavaScript 專案中使用 JSDoc 為 TypeScript 提供型別支援的能力。從現在開始,為你的程式碼加上 清晰、可維護的型別註解,讓開發效率與程式品質同步提升吧!