TypeScript 型別收窄 (Type Narrowing)
如果說 TypeScript 是你的程式碼保鑣,那「型別收窄 (Type Narrowing)」就是它的「邏輯推理能力」。
想像一下,你有一個變數可能是 string 也可能是 number。當你在程式碼中寫了 if 判斷,TypeScript 會像福爾摩斯一樣,根據你寫的邏輯,動態地推斷出在某個區塊內,這個變數一定是什麼型別。這個過程就叫做「收窄」。
最常見的收窄方式
typeof 檢查
這是最直覺的方法。
function padLeft(padding: number | string, input: string) {
// 此時 padding 還是 number | string
if (typeof padding === 'number') {
// TypeScript 知道進入這個區塊,padding 一定是 number
return ' '.repeat(padding) + input;
}
// TypeScript 知道如果能執行到這,padding 一定是 string
// (因為如果是 number,上面就 return 走了)
return padding + input;
}
真值收窄 (Truthiness Narrowing)
利用 JavaScript 在 if 中會將值轉為 boolean 的特性。
function printAll(strs: string | string[] | null) {
if (strs) {
// 這裡排除了 null (因為 null 是 falsy)
// 但strs 仍可能是 string 或 string[]
if (typeof strs === 'object') {
// 被收窄為 string[] (因為 string 不是 object)
for (const s of strs) {
console.log(s);
}
} else {
// 被收窄為 string
console.log(strs);
}
}
}
小心陷阱:空字串
""和數字0在 JavaScript 中也是 falsy。如果你只是想排除null,直接用!= null會更安全。
相等性收窄 (Equality Narrowing)
使用 === 或 !==。
function example(x: string | number, y: string | boolean) {
if (x === y) {
// 如果 x 等於 y,那它們一定都是 string
// 因為 number 和 boolean 不可能全等
x.toUpperCase();
y.toUpperCase();
}
}
in 運算子收窄
用來檢查物件中是否有某個屬性。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) {
// 有 swim 的一定是 Fish
animal.swim();
} else {
// 否則那就是 Bird
animal.fly();
}
}
instanceof 收窄
檢查是否為某個類別的實例,常用於處理 Date 或自訂類別。
function logValue(x: Date | string) {
if (x instanceof Date) {
// x 是 Date
console.log(x.toUTCString());
} else {
// x 是 string
console.log(x.toUpperCase());
}
}
控制流程分析 (Control Flow Analysis)
TypeScript 的分析能力非常強大,它會追蹤程式的執行路徑。
function example() {
let x: string | number | boolean;
x = Math.random() < 0.5;
// 現在 x 是 boolean
if (Math.random() < 0.5) {
x = 'hello';
// 現在 x 是 string
} else {
x = 100;
// 現在 x 是 number
}
return x; // 回傳 string | number
}
Discriminated Unions (可辨識聯合)
這是處理複雜型別最優雅的方式。我們給每個介面一個共同的欄位(通常叫 kind 或 type)來當作標籤。
interface Circle {
kind: 'circle'; // 字面量型別
radius: number;
}
interface Square {
kind: 'square'; // 字面量型別
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
// 透過檢查共同欄位 kind
switch (shape.kind) {
case 'circle':
// shape 自動收窄為 Circle
return Math.PI * shape.radius ** 2;
case 'square':
// shape 自動收窄為 Square
return shape.sideLength ** 2;
}
}
總結
型別收窄是 TypeScript 讓我們寫出既安全又像原生 JavaScript 一樣靈活的程式碼的關鍵。
你不必總是顯式地轉型 (as),只要你的邏輯判斷是合理的(例如用了 typeof, if, switch),TypeScript 通常都能理解你想做什麼,並自動幫你把型別變精確。