本文 AI 產出,尚未審核
TypeScript – 型別推論與型別保護(Union Narrowing)
簡介
在日常的 JavaScript 開發中,我們常會遇到「一個變數可能是多種型別」的情況,例如 API 回傳的資料可能是 string 或 null,或是函式參數可以接受 number | string。
TypeScript 透過 Union 型別 讓我們可以明確描述這類「或」的關係,但僅僅寫出 type T = A | B 並不足以讓編譯器在使用時自動判斷到底是哪一個型別。
Union narrowing(聯合型別的收窄)則是 TypeScript 的型別保護機制之一,能在程式執行流程中根據條件判斷把寬泛的 Union 型別「縮小」成更具體的單一型別。掌握這項技巧,能讓程式碼在編譯階段就捕捉到潛在的錯誤,同時保留良好的開發體驗(自動完成、型別提示)。
核心概念
1. 什麼是 Union Narrowing?
Union narrowing 指的是 在程式的控制流程裡,透過條件判斷(if、switch、in、typeof、instanceof 等)把 A | B | C 這樣的聯合型別縮減為單一的 A、B 或 C。
縮窄後,編譯器會把變數的型別視為被縮窄的那一個,從而允許存取該型別特有的屬性或方法。
function foo(x: string | number) {
// 這裡 x 仍是 string | number
if (typeof x === "string") {
// 這裡 x 被縮窄為 string
console.log(x.toUpperCase());
} else {
// 這裡 x 被縮窄為 number
console.log(x.toFixed(2));
}
}
2. 常見的型別保護方式
| 保護方式 | 說明 | 範例 |
|---|---|---|
typeof |
用於基本型別(string、number、boolean、symbol、bigint、function) |
if (typeof v === "number") … |
instanceof |
判斷物件是否為某個建構子(class)的實例 | if (obj instanceof Date) … |
in |
檢查屬性是否存在於物件上 | if ("length" in arr) … |
| 自訂型別保護(type guard) | 使用返回值類型為 x is Y 的函式 |
function isUser(o: any): o is User { … } |
Array.isArray |
判斷是否為陣列(特殊的型別保護) | if (Array.isArray(v)) … |
3. 範例一:基本型別的縮窄
function format(value: string | number | boolean) {
if (typeof value === "string") {
// value 為 string
return `"${value}"`;
}
if (typeof value === "number") {
// value 為 number
return value.toLocaleString();
}
// 只剩下 boolean
return value ? "YES" : "NO";
}
- 重點:
typeof能直接把 Union 中的基本型別分離,縮窄後即可安全使用該型別的方法。
4. 範例二:in 关键字檢查屬性
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
function area(shape: Circle | Square) {
if ("radius" in shape) {
// shape 被視為 Circle
return Math.PI * shape.radius ** 2;
} else {
// shape 被視為 Square
return shape.side ** 2;
}
}
- 技巧:
in只要檢查屬名是否存在,就能把聯合型別依屬性分割。這在「同一屬性名稱不會同時出現在不同型別」時特別好用。
5. 範例三:自訂型別保護(type guard)
interface Dog {
kind: "dog";
bark(): void;
}
interface Cat {
kind: "cat";
meow(): void;
}
type Pet = Dog | Cat;
function isDog(pet: Pet): pet is Dog {
return pet.kind === "dog";
}
function talk(pet: Pet) {
if (isDog(pet)) {
// pet 被縮窄為 Dog
pet.bark();
} else {
// pet 被縮窄為 Cat
pet.meow();
}
}
- 說明:自訂的
isDog函式回傳型別謂詞pet is Dog,讓 TypeScript 在if (isDog(pet))裡自動把pet縮窄為Dog。
6. 範例四:instanceof 與類別
class Bird {
fly() { console.log("fly"); }
}
class Fish {
swim() { console.log("swim"); }
}
type Animal = Bird | Fish;
function move(a: Animal) {
if (a instanceof Bird) {
a.fly(); // a 為 Bird
} else {
a.swim(); // a 為 Fish
}
}
- 注意:
instanceof只能用在有實體建構子的類別,對於純介面(interface)無效。
7. 範例五:結合 Array.isArray 與聯合型別
function flatten(input: number | number[] | number[][]) {
if (Array.isArray(input)) {
// input 為 number[] 或 number[][]
return input.flat(2);
}
// input 為單一 number
return [input];
}
- 重點:
Array.isArray是檢查「是否為陣列」的唯一型別保護,適用於任意深度的陣列型別。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 |
|---|---|---|
| 屬性名稱重疊 | 若兩個型別都擁有相同屬名,in 無法正確縮窄。 |
使用 kind 標籤或自訂 type guard。 |
typeof 無法辨別 null |
typeof null 會回傳 "object",容易與其他物件混淆。 |
先檢查 value === null 再使用 typeof。 |
instanceof 只對 class 有效 |
介面或型別別名無法使用 instanceof。 |
改用 in 或自訂型別保護。 |
過度依賴 any |
把 Union 變成 any 會失去縮窄的好處。 |
盡量保留具體型別,使用 unknown 搭配 type guard。 |
忘記返回 never |
在 switch 完整性檢查時,未處理的情況會回傳 never,導致錯誤被忽略。 |
在 default 中拋出錯誤或回傳 never 以保證完整性。 |
最佳實踐
- 使用 discriminated unions(具標籤的聯合型別):在每個型別加入唯一的
kind或type屬性,讓switch/if判斷更直觀。 - 盡量寫自訂 type guard:可重用且易於測試,尤其在大型專案中。
- 保持型別的可讀性:過於複雜的 Union 會降低可維護性,必要時可拆分為多個小型 Union。
- 結合
never進行完整性檢查:在switch的default中使用assertNever(value),確保未來加入新型別時會得到編譯錯誤提醒。
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
實際應用場景
API 回傳的多型別結果
type ApiResponse = { status: "ok"; data: User } | { status: "error"; error: string }; function handle(resp: ApiResponse) { if (resp.status === "ok") { // resp 被縮窄為 { status: "ok"; data: User } console.log(resp.data.name); } else { // resp 為 error 形態 console.error(resp.error); } }表單欄位的多種輸入型別
type InputValue = string | number | Date; function formatInput(v: InputValue) { if (v instanceof Date) return v.toISOString(); if (typeof v === "number") return v.toLocaleString(); return v.trim(); }React 組件的 Props 多型別
type ButtonProps = { type: "primary"; onClick: () => void } | { type: "link"; href: string }; const Button = (props: ButtonProps) => { if (props.type === "primary") { return <button onClick={props.onClick}>Primary</button>; } return <a href={props.href}>Link</a>; };資料處理管線的不同階段
在資料流中,每個階段可能傳遞string | Buffer | null,使用if (value !== null && typeof value === "string")能安全地執行文字處理。
總結
- Union narrowing 是 TypeScript 型別保護的核心機制,讓我們可以在執行流程中把寬泛的聯合型別縮減為具體型別。
- 常見的縮窄手段包括
typeof、instanceof、in、Array.isArray以及 自訂 type guard。 - 正確使用縮窄不僅能提升編譯時安全性,也能讓 IDE 提供更精準的自動完成與錯誤提示。
- 在實務開發中,建議採用 discriminated unions、撰寫可重用的 type guard,並配合
never完整性檢查,避免常見的陷阱。
掌握了 Union narrowing,您就能在 TypeScript 專案裡寫出更安全、可讀、易維護的程式碼,並充分發揮靜態型別的威力。祝開發順利!