本文 AI 產出,尚未審核

類別型別保護(Class Type Guard)

簡介

在 TypeScript 中,型別推論 能讓編譯器自動判斷變數的型別,而 型別保護(type narrowing) 則是把較寬鬆的型別縮小成更具體的型別,讓程式在執行階段能獲得更好的安全性與自動補完。
當我們在面對多型(polymorphism)或是同一個變數可能是多種不同類別的情況時,僅靠屬性檢查往往不足——此時 類別型別保護 就派上用場。透過 instanceofin、自訂型別保護函式或是私有屬性等技巧,我們可以在執行時確定變數究竟是哪一個具體類別,進而安全地呼叫對應的方法或屬性。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶出實務應用,讓你在日常開發中能熟練運用類別型別保護,寫出更可靠的 TypeScript 程式碼。


核心概念

1. 為什麼需要類別型別保護?

在 OOP 中,我們常會把父類別(或介面)作為參數或屬性型別,然後在函式內根據實際傳入的子類別執行不同的邏輯。若沒有型別保護,編譯器只能看到「父類別」的型別資訊,會阻止我們直接存取子類別特有的屬性或方法。

例子Animal 為基底類別,DogCat 為子類別。若函式接受 Animal,想要呼叫 bark()(只屬於 Dog)就必須先確定實際傳入的是 Dog

2. instanceof – 最直觀的類別保護

instanceof 是 JavaScript 原生提供的運算子,會在執行時檢查物件的原型鏈。TypeScript 能夠根據 instanceof 的結果自動縮小型別。

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  bark() {
    console.log(`${this.name} says: Woof!`);
  }
}

class Cat extends Animal {
  meow() {
    console.log(`${this.name} says: Meow!`);
  }
}

function speak(animal: Animal) {
  if (animal instanceof Dog) {
    // 在此分支裡,animal 被縮小為 Dog
    animal.bark();               // ✅ 安全呼叫
  } else if (animal instanceof Cat) {
    animal.meow();               // ✅ 安全呼叫
  } else {
    console.log(`${animal.name} makes no sound.`);
  }
}

重點instanceof 只能用於 具名類別(有 constructor function),對於介面或型別別名無效。


3. in 運算子 – 檢查屬性是否存在

有時候我們只想根據某個屬性是否出現在物件上來判斷類別,in 可以做到這一點。配合 TypeScript 的 映射型別保護,編譯器同樣會縮小型別。

interface Square {
  kind: "square";
  size: number;
}

interface Circle {
  kind: "circle";
  radius: number;
}

type Shape = Square | Circle;

function area(shape: Shape): number {
  if ("size" in shape) {
    // 此時 shape 被推斷為 Square
    return shape.size * shape.size;
  } else {
    // 此時 shape 被推斷為 Circle
    return Math.PI * shape.radius ** 2;
  }
}

提示in 只適用於 屬性名稱是字面量 的情況,且必須保證屬性在所有可能的型別中不會同時出現。


4. 自訂類別型別保護函式

instanceofin 無法滿足需求時,我們可以自行撰寫 型別保護函式(type guard function),其返回值必須是形如 param is SpecificType 的型別謂詞。

class Bird {
  fly() {
    console.log("I can fly!");
  }
}

class Penguin {
  swim() {
    console.log("I can swim!");
  }
}

// 型別保護函式
function isBird(creature: Bird | Penguin): creature is Bird {
  // 只要有 fly 方法就視為 Bird
  return (creature as Bird).fly !== undefined;
}

function move(creature: Bird | Penguin) {
  if (isBird(creature)) {
    // 這裡 creature 已被縮小為 Bird
    creature.fly();   // ✅ 安全呼叫
  } else {
    // 這裡 creature 被縮小為 Penguin
    creature.swim();  // ✅ 安全呼叫
  }
}

技巧:型別保護函式內部可以使用任意的 runtime 檢查(typeofinstanceof、屬性檢查等),只要回傳 boolean 並在函式簽名中寫明 param is TargetType


5. 私有或受保護屬性作為隱形標記

在某些情況下,我們希望類別之間的型別保護是 不可被外部偽造 的。利用 私有欄位private)或 受保護欄位protected)作為類別的隱形標記,配合 instanceof 或自訂保護函式,可避免跨檔案手動造假。

class Vehicle {
  // 私有屬性僅在同一類別內可見,外部無法直接檢測
  private readonly __vehicleBrand!: string;
}

class Car extends Vehicle {
  drive() {
    console.log("Driving a car");
  }
}

class Bike extends Vehicle {
  pedal() {
    console.log("Pedaling a bike");
  }
}

// 使用 instanceof + private 標記的保護函式
function isCar(v: Vehicle): v is Car {
  return v instanceof Car;
}

function operate(v: Vehicle) {
  if (isCar(v)) {
    v.drive();   // ✅ 安全呼叫
  } else {
    (v as Bike).pedal(); // 仍需要斷言,或寫另一個保護函式
  }
}

要點:私有屬性不會出現在型別宣告的結構中,但仍會在執行時隨實例存在,instanceof 能正確辨識子類別。


6. 抽象類別與受保護建構子

抽象類別本身不能直接被實例化,常用來定義共通介面。當抽象類別的建構子被標記為 protected 時,只有子類別才能呼叫,這也為型別保護提供了額外的安全層。

abstract class Shape {
  protected constructor(public readonly name: string) {}
  abstract area(): number;
}

class Rectangle extends Shape {
  constructor(public width: number, public height: number) {
    super("rectangle");
  }
  area() {
    return this.width * this.height;
  }
}

class Triangle extends Shape {
  constructor(public base: number, public height: number) {
    super("triangle");
  }
  area() {
    return (this.base * this.height) / 2;
  }
}

function printArea(s: Shape) {
  // 直接使用抽象類別的抽象方法,不需要額外保護
  console.log(`${s.name} area = ${s.area()}`);
}

實務意義:抽象類別配合 instanceof 可在大型系統中快速定位子類別,同時保證只能由合法子類別產生實例。


常見陷阱與最佳實踐

陷阱 說明 解決方案
使用 instanceof 判斷介面 介面在編譯後會被擦除,instanceof 永遠回傳 false 改用屬性檢查 (in) 或自訂型別保護函式。
屬性名稱重疊導致誤判 多個型別同時擁有相同屬性,in 會產生錯誤的縮小結果。 使用 辨別聯合類型(discriminated union)或加入額外的區分屬性(如 kind)。
私有屬性被外部斷言繞過 as 斷言可以強制把任何物件視為某類別,削弱型別保護。 在公共 API 中盡量避免接受 any/unknown,或在函式內部使用自訂保護函式再次驗證。
過度依賴 any 一旦進入 any,型別推論即失效,保護機制無法發揮。 儘量使用 unknown,配合型別保護函式轉換為具體型別。
忘記回傳型別謂詞 自訂保護函式若忘記寫 param is Target,編譯器不會縮小型別。 使用 IDE 快速模板或 lint 規則(如 typescript-eslint)提醒。

最佳實踐

  1. 先選擇語意最清晰的保護方式instanceofin → 自訂保護函式。
  2. 在聯合型別中加入 kind 或類似辨別屬性,避免屬性重疊。
  3. 保持保護函式純粹:僅做型別檢查,不執行副作用。
  4. 對外部輸入使用 unknown,在入口處立即做型別保護,減少污染整個程式碼基底。
  5. 寫單元測試:特別是自訂保護函式的邊界情況,確保未來重構不會破壞型別安全。

實際應用場景

1. 前端表單驗證

在大型表單中,每個欄位可能對應不同的驗證類別(RequiredValidatorEmailValidatorNumberRangeValidator)。透過 instanceofkind 屬性,我們可以在迭代驗證器陣列時正確呼叫對應的 validate() 方法。

2. UI 元件庫的多樣化渲染

React/Vue 的元件庫常會接受 props 為多種子類別(例如 ButtonPropsLinkProps)。使用類別型別保護,我們能在單一渲染函式內安全地判斷並返回正確的 JSX/TSX。

3. 後端服務的指令處理(Command Pattern)

指令物件 (CreateUserCommandDeleteUserCommand) 繼承自抽象 Command。在指令分派器(dispatcher)中,使用 instanceof 直接把指令路由到相應的處理器,避免繁雜的 switch

4. 資料流(Stream)與事件系統

事件基礎設計常會有 MouseEventKeyboardEventTouchEvent 等子類別。透過 in 檢查 buttonkey 等屬性,或自訂保護函式,能在事件處理器中安全取得特定屬性。

5. 微服務間的訊息協定

跨服務傳遞的訊息可能是 UserCreatedMessageOrderPlacedMessage 等,皆實作同一個介面 Message。在接收端使用 kind 辨別或 instanceof(若使用 class)來正確解碼與處理。


總結

類別型別保護是 TypeScript 型別系統JavaScript 執行時 互相配合的關鍵技巧。透過 instanceofin、自訂型別保護函式以及私有/受保護屬性的巧妙運用,我們可以在不犧牲程式彈性的前提下,取得 編譯期的型別安全執行期的行為保證

在實務開發中,建議遵循以下步驟:

  1. 先檢查是否有明顯的辨別屬性(kind,若有則直接使用 inswitch
  2. 若類別具備構造函式,優先使用 instanceof
  3. 當上述方式皆不適用,自行撰寫型別保護函式,並確保其返回型別謂詞。
  4. 在公共 API 處理外部資料時,使用 unknown + 型別保護,避免 any 的污染。
  5. 持續寫測試,確保保護邏輯在未來的重構或擴充中仍然正確。

掌握了類別型別保護,你的 TypeScript 程式將更安全可讀易於維護,也能更自信地在大型專案中運用多型與抽象概念。祝你寫程式快快樂樂,型別安全永相伴!