本文 AI 產出,尚未審核
TypeScript:型別推論與型別保護(Type Inference & Narrowing)
主題 – 自訂型別保護函式(is Type Predicate)
簡介
在 JavaScript 專案逐步導入 TypeScript 時,最常碰到的挑戰之一就是 如何在執行時正確辨識變數的真實型別。
即使 TypeScript 具備強大的型別推論機制,編譯器仍只能根據靜態資訊做判斷;當資料來源是外部 API、JSON、或是使用者輸入時,編譯器無法自行保證型別安全。
此時 自訂型別保護函式(又稱 type predicate)就派上用場。透過 parameterName is TargetType 的語法,我們可以告訴編譯器「在函式返回 true 後,參數的型別一定是 TargetType」,從而讓後續程式碼得到正確的型別推斷與 IntelliSense 支援。
本篇文章將以淺顯易懂的方式說明什麼是型別保護、如何撰寫自訂保護函式、常見陷阱與最佳實踐,並提供實務範例,幫助你在日常開發中更安全、更有效率地使用 TypeScript。
核心概念
1. 為何需要型別保護?
- 執行時型別不確定:
any、unknown、或從外部取得的資料在編譯時無法確定具體形狀。 - 控制流程影響型別:
if (Array.isArray(x)) { … }這類檢查會讓 TypeScript 在分支內部自動縮窄(narrow)變數型別。 - 自訂檢查:內建的
Array.isArray、typeof、instanceof只能處理有限情況,對於自訂介面或複合型別,需要自行撰寫保護函式。
2. 型別保護的語法
function isFoo(value: unknown): value is Foo {
// 判斷邏輯
}
value為被檢查的參數。value is Foo為 type predicate,告訴編譯器「若此函式回傳true,value必為Foo型別」。
3. 基本範例
範例 1:檢查是否為 string
function isString(val: unknown): val is string {
return typeof val === "string";
}
// 使用
function printLength(x: unknown) {
if (isString(x)) {
// 此處 x 已被縮窄為 string
console.log("長度:", x.length);
} else {
console.log("不是字串");
}
}
範例 2:判斷物件是否符合介面 User
interface User {
id: number;
name: string;
email?: string;
}
function isUser(obj: unknown): obj is User {
if (typeof obj !== "object" || obj === null) return false;
const u = obj as Partial<User>;
return typeof u.id === "number" && typeof u.name === "string";
}
// 使用
function greet(person: unknown) {
if (isUser(person)) {
console.log(`Hello, ${person.name}!`);
} else {
console.log("無效的使用者資料");
}
}
範例 3:辨識聯合型別 string | number
function isNumber(val: unknown): val is number {
return typeof val === "number";
}
function process(value: string | number) {
if (isNumber(value)) {
// value 被視為 number
console.log(value.toFixed(2));
} else {
// 此分支則是 string
console.log(value.toUpperCase());
}
}
範例 4:檢查陣列內元素型別(深度保護)
function isStringArray(arr: unknown): arr is string[] {
return Array.isArray(arr) && arr.every(item => typeof item === "string");
}
// 使用
function joinStrings(data: unknown) {
if (isStringArray(data)) {
// data 已被縮窄為 string[]
console.log(data.join(", "));
} else {
console.log("不是字串陣列");
}
}
範例 5:自訂類別的型別保護
class Circle {
constructor(public radius: number) {}
area() { return Math.PI * this.radius ** 2; }
}
function isCircle(obj: unknown): obj is Circle {
return obj instanceof Circle;
}
// 使用
function calcArea(shape: unknown) {
if (isCircle(shape)) {
console.log("圓面積:", shape.area());
} else {
console.log("非 Circle 物件");
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
返回 any 或 unknown |
若保護函式回傳 boolean 但未使用 is Type,編譯器不會縮窄型別。 |
必須明確寫成 param is Target 的 type predicate。 |
| 檢查不完整 | 只檢查了部分屬性,導致型別仍可能缺失。 | 檢查所有必須屬性,或使用 Partial<T> 先做型別斷言,再逐屬性驗證。 |
使用 instanceof 檢查介面 |
介面在編譯後會消失,instanceof 永遠回傳 false。 |
只對真正的 class 使用 instanceof,介面則使用屬性檢查或 in 關鍵字。 |
過度依賴 any |
在保護函式內部使用 any 會失去型別安全的意義。 |
盡量使用 unknown 作為輸入型別,並在函式內部逐步縮窄。 |
| 忘記匯出保護函式 | 在多檔案專案中,若保護函式未匯出,其他模組無法使用。 | 使用 export 並在 tsconfig.json 設定 isolatedModules 為 true,確保每個檔案都是獨立模組。 |
最佳實踐
- 保持函式純粹:型別保護函式只負責判斷,不應該有副作用。
- 使用
unknown作為參數型別:這樣能強迫呼叫端先進行型別檢查。 - 提供完整註解:說明保護的條件與限制,方便同事閱讀。
- 結合
asserts斷言:若想在保護成功時拋出例外,可使用asserts value is T。
function assertIsUser(obj: unknown): asserts obj is User {
if (!isUser(obj)) throw new Error("Invalid User");
}
實際應用場景
API 回傳資料驗證
- 從第三方服務取得 JSON,透過自訂保護函式確保資料符合預期介面,避免 runtime error。
表單輸入檢查
- 使用
isString,isNumber等保護函式在提交前快速縮窄型別,讓後端型別映射更安全。
- 使用
多態函式實作
- 在接受
Shape = Circle | Rectangle的函式內部,用isCircle、isRectangle依序縮窄,實現乾淨的分支邏輯。
- 在接受
插件系統
- 核心框架只接受
unknown的插件物件,透過插件提供的isPlugin保護函式驗證,確保插件符合介面規範。
- 核心框架只接受
總結
- 自訂型別保護函式 是 TypeScript 讓編譯器在執行時也能正確推論型別的關鍵工具。
- 只要遵守
parameter is TargetType的語法,配合完整的屬性檢查,就能在if、switch等控制流程中自動縮窄型別。 - 注意避免檢查不完整、誤用
instanceof、以及在保護函式內部使用any,這些都是常見的陷阱。 - 在實務開發中,無論是 API 資料驗證、表單處理或多型設計,都能透過型別保護提升程式的安全性與可維護性。
掌握 型別保護,你就能在 TypeScript 的型別系統與 JavaScript 的動態特性之間架起一座堅固的橋樑,寫出更可靠、更具自信的程式碼。祝開發順利!