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.id、data.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>,讓函式支援任意型別的陣列。- 透過 JSDoc,
filter的回傳型別會自動與輸入陣列保持一致。
範例 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 中正確設定 module、esModuleInterop。 |
忽略 @typedef 的作用域 |
@typedef 若寫在函式內部,只在該函式可見。 |
將共用型別放在檔案最上方或獨立的 .d.ts 檔,確保全域可見。 |
最佳實踐:
- 逐步遷移:先在新開發的檔案加入 JSDoc,逐漸覆蓋舊有程式碼。
- 使用
/** @ts-ignore */只在必要時暫時忽略型別錯誤,避免濫用。 - 配合 lint 工具:如
eslint-plugin-jsdoc可自動檢查 JSDoc 格式與完整性。 - 文件即程式碼:將 JSDoc 視為 API 文件,保持與實作同步,提升團隊協作效率。
實際應用場景
引入第三方 JavaScript 套件
某些套件(如舊版的lodash、moment)沒有官方的.d.ts,可自行在node_modules旁建立*.js檔案,加入 JSDoc 描述,讓 TypeScript 編譯器即時提供型別資訊。遺留系統的漸進式升級
大型企業常有多年累積的 JavaScript 程式碼,直接改寫成本過高。透過 JSDoc,開發者可在不改變執行行為的前提下,逐步為關鍵模組加上型別。前端框架的自訂指令或插件
在 Vue、React 等框架中,開發自訂指令或高階元件時,使用 JSDoc 為 props、context、hook 回傳值標註型別,可讓使用者在 IDE 中得到即時提示,降低使用錯誤。Node.js 後端服務
Express、Koa 等輕量框架多以 JavaScript 撰寫,配合 JSDoc 為req.body、res.locals等屬性標註,可在撰寫路由處理函式時得到完整的型別支援。
總結
- JSDoc 為 JavaScript 提供了一條低侵入、易上手的型別描述管道,讓 TypeScript 能在混合專案中發揮完整的型別檢查與 IDE 支援。
- 掌握 基本標籤(
@param、@returns、@type)與 進階技巧(@typedef、@template、@enum),即可在日常開發中快速為函式、物件、類別加上型別。 - 識別常見陷阱、遵守最佳實踐,能避免註解與程式碼不同步、過度使用
any等問題,確保型別資訊的可靠性。 - 在 第三方套件、遺留系統、框架插件與 Node.js 後端 等實務情境中,JSDoc 是將 JavaScript 平滑過渡到 TypeScript 的關鍵橋樑。
透過本文的概念與範例,你已具備在任何 JavaScript 專案中使用 JSDoc 為 TypeScript 提供型別支援的能力。從現在開始,為你的程式碼加上 清晰、可維護的型別註解,讓開發效率與程式品質同步提升吧!