昨天我們玩了泛型進階,今天要解決一個很常見的痛點:
當一個變數可能是多種型別(Union Type)時,你要怎麼告訴 TS「這裡是那個型別」?
這就是 型別守衛(Type Guards) 的用途。
它讓 TS「聽得懂」你的 if/else,幫你在區塊內精準推論型別。
先看沒用型別守衛的痛苦:
function printLength(value: string | string[]) {
console.log(value.length);
// ❌ Property 'length' exists on both types,
// but TS 不知道你是不是一定有 length?
}
雖然這個例子可以過,但一旦方法只存在於其中一種型別,問題就來了:
function printUpperCase(value: string | string[]) {
console.log(value.toUpperCase());
// ❌ Property 'toUpperCase' does not exist on type 'string[]'
}
TS 在保護你:「萬一 value 是陣列,toUpperCase 就爆了」。
typeof
型別守衛適合檢查 基本型別(string、number、boolean、symbol、bigint、undefined)。
function printId(id: string | number) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // 這裡 TS 知道 id 是 string
} else {
console.log(id.toFixed(2)); // 這裡 TS 知道 id 是 number
}
}
instanceof
型別守衛適合檢查 物件是否是某個類別的實例。
class Dog { bark() {} }
class Cat { meow() {} }
function makeSound(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark();
} else {
pet.meow();
}
}
in
型別守衛適合檢查 屬性是否存在於物件(常用於物件型 Union)。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim();
} else {
animal.fly();
}
}
讓你寫一個函式,回傳值型別是 paramName is Type
,
TS 會把它視為型別守衛條件。
type Car = { drive: () => void };
type Boat = { sail: () => void };
function isCar(vehicle: Car | Boat): vehicle is Car {
return (vehicle as Car).drive !== undefined;
}
function start(vehicle: Car | Boat) {
if (isCar(vehicle)) {
vehicle.drive();
} else {
vehicle.sail();
}
}
好處:
假設 API 回傳可能成功或失敗:
type Success = { ok: true; data: string };
type Failure = { ok: false; error: string };
function handleResponse(res: Success | Failure) {
if (res.ok) {
// 這裡 TS 自動推論 res: Success
console.log("Data:", res.data);
} else {
// 這裡 TS 自動推論 res: Failure
console.log("Error:", res.error);
}
}
關鍵是 ok 是一個字面量型別(literal type),TS 會用它來做型別窄化(Discriminated Union)。
function isCarBad(vehicle: any): vehicle is Car {
return !!vehicle.drive; // 這樣只檢查 truthy,可能誤判
}
修正:要確認型別真的對:
function isCarGood(vehicle: any): vehicle is Car {
return typeof vehicle.drive === "function";
}
function processAnimal(a: Fish | Bird | Dog) {
if ("swim" in a) {
a.swim();
} else if ("fly" in a) {
a.fly();
}
// ❌ Dog 沒被處理
}
修正:用 never
兜底,確保處理完整:
function assertNever(x: never): never {
throw new Error("Unexpected type: " + x);
}
function processAnimal(a: Fish | Bird | Dog) {
if ("swim" in a) {
a.swim();
} else if ("fly" in a) {
a.fly();
} else {
assertNever(a); // 如果多了新型別,編譯期就報錯
}
}