iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Modern Web

我與型別的 30 天約定:TypeScript 入坑實錄系列 第 13

Day 13|型別守衛(Type Guards):幫 TypeScript 看懂你的 if/else

  • 分享至 

  • xImage
  •  

昨天我們玩了泛型進階,今天要解決一個很常見的痛點:

當一個變數可能是多種型別(Union Type)時,你要怎麼告訴 TS「這裡是那個型別」?

這就是 型別守衛(Type Guards) 的用途。

它讓 TS「聽得懂」你的 if/else,幫你在區塊內精準推論型別。


1. 為什麼需要型別守衛?

先看沒用型別守衛的痛苦:

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 就爆了」。


2. 型別守衛的四種常見方式

2.1 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
  }
}

2.2 instanceof 型別守衛

適合檢查 物件是否是某個類別的實例

class Dog { bark() {} }
class Cat { meow() {} }

function makeSound(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark();
  } else {
    pet.meow();
  }
}

2.3 in 型別守衛

適合檢查 屬性是否存在於物件(常用於物件型 Union)。

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim();
  } else {
    animal.fly();
  }
}

2.4 自訂型別守衛(User-Defined Type Guards)

讓你寫一個函式,回傳值型別是 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();
  }
}

好處:

  • 可重複使用判斷邏輯
  • 讓 TS 在 if 區塊內自動縮小型別

3. 實務案例:安全處理 API 回傳值

假設 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)。


4. 常見錯誤與修正

4.1 判斷不夠嚴謹

function isCarBad(vehicle: any): vehicle is Car {
  return !!vehicle.drive; // 這樣只檢查 truthy,可能誤判
}

修正:要確認型別真的對:

function isCarGood(vehicle: any): vehicle is Car {
  return typeof vehicle.drive === "function";
}

4.2 忘了處理所有型別分支

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); // 如果多了新型別,編譯期就報錯
  }
}

5. 心法小結

  • 型別守衛的目的是 讓 TS 聽懂你的判斷邏輯,在特定區塊內窄化型別。
  • typeof:基本型別
  • instanceof:class 實例
  • in:檢查屬性存在
  • 自訂守衛:複雜情況可重複利用
  • 字面量屬性判斷(Discriminated Union):最直觀也最安全

上一篇
Day 12|泛型進階:預設值、多參數與更嚴格的約束
下一篇
Day 14|Utility Types:TS 內建的型別小工具大全
系列文
我與型別的 30 天約定:TypeScript 入坑實錄14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言