TypeScript 型別守衛 (Type Guards)

TypeScript 的編譯器非常聰明,但在某些情況下,它無法自動判斷一個變數的確切型別,這時候我們就需要「型別守衛 (Type Guards)」。

簡單來說,型別守衛就是一個回傳 boolean 的函式,但它多了一個特殊的功用:告訴 TypeScript 編譯器「如果這個函式回傳 true,那這個變數就是某某型別」。

為什麼需要型別守衛?

假設我們有兩種動物:

interface Fish {
  swim(): void;
}
interface Bird {
  fly(): void;
}

function move(animal: Fish | Bird) {
  // 這裡 TypeScript 只知道 animal 可能是 Fish 或 Bird
  // 所以你不能直接呼叫 swim() 或 fly(),因為不確定它到底是哪一種
  // animal.swim(); // 錯誤!Bird 不會游泳
}

我們可以用 instanceofin 來檢查,但如果邏輯比較複雜,我們通常會把它封裝成一個函式。

但問題來了:TypeScript 編譯器不會去分析你函式裡面的邏輯

// 普通的檢查函式
function isFish(animal: Fish | Bird): boolean {
  return (animal as Fish).swim !== undefined;
}

// 即使 isFish 回傳 true,TypeScript 還是不知道 animal 是 Fish
if (isFish(animal)) {
  // animal.swim(); // 還是錯誤!
}

這就是為什麼我們需要特殊的語法:Type Predicate (型別謂詞)

自訂型別守衛 (parameter is Type)

我們只需要把回傳型別從 boolean 改成 arg is Type

// 關鍵在於回傳值:animal is Fish
function isFish(animal: Fish | Bird): animal is Fish {
  return (animal as Fish).swim !== undefined;
}

if (isFish(animal)) {
  // 這裡 animal 被正確收窄為 Fish
  animal.swim(); // OK!
} else {
  // 這裡 TypeScript 很聰明地推論出,如果不適 Fish,那一定是 Bird
  animal.fly(); // OK!
}

語法重點

function isString(value: unknown): value is string {
  return typeof value === 'string';
}
  1. 函式必須回傳 boolean
  2. 回傳型別必須寫成 參數名 is 型別
  3. 這個語法只能用在回傳 boolean 的函式上。

進階:斷言函式 (Assertion Functions)

有時候我們不想要回傳 boolean,而是希望「如果不符合型別就直接丟出錯誤 (Throw Error)」。這在寫測試或驗證 API 資料時很常用。

TypeScript 3.7 推出了 asserts 關鍵字。

// 注意:asserts value is string
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Value must be a string');
  }
}

function process(input: unknown) {
  assertIsString(input);
  // 程式能執行到這行,代表沒有丟出錯誤
  // 所以 input 一定是 string
  console.log(input.toUpperCase()); // OK
}

實用範例:過濾陣列中的 Null

這是一個非常經典的案例。假設我們有一個混雜 null 的陣列:

const items = ['a', null, 'b', undefined, 'c'];
// items 型別是 (string | null | undefined)[]

// 我們想過濾掉 null 和 undefined
// 錯誤做法:
const strings = items.filter((item) => item != null);
// strings 的型別其實還是 (string | null | undefined)[]
// 因為 Array.prototype.filter 不會自動變更型別

正確做法是搭配型別守衛:

// 定義一個守衛
function isNotNull<T>(value: T | null | undefined): value is T {
  return value != null;
}

const strings = items.filter(isNotNull);
// 現在 strings 被正確推論為 string[]

總結

型別守衛是連接「執行時期邏輯」與「編譯時期型別」的橋樑。

  • 當 TypeScript 無法自動判斷型別時,寫一個型別守衛函式。
  • 使用 arg is Type 語法告訴編譯器判斷結果。
  • 使用 asserts arg is Type 來處理拋出錯誤的驗證邏輯。