本文 AI 產出,尚未審核

TypeScript

單元:變數與常數宣告(Variables)

主題:Type Guards(型別保護)


簡介

在 JavaScript 中,變數的型別是動態決定的,這讓程式在執行時容易出現不可預期的錯誤。TypeScript 透過 靜態型別檢查,在編譯階段就能捕捉大部份問題,但仍有一些情況是 型別不確定(any、聯合型別 string | number 等),此時若直接使用變數的屬性或方法,編譯器會發出警告。

型別保護(Type Guard) 正是解決這類問題的關鍵技術:它讓 TypeScript 在程式碼的特定分支中「窄化」變數的型別,使編譯器能正確推斷後續的操作是否合法。掌握型別保護,不僅能提升程式的安全性,也能讓 IDE 提供更完整的自動完成與即時錯誤提示,對於 初學者到中級開發者 都是必備的技巧。


核心概念

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

當變數的型別是 聯合型別(Union Type)時,TypeScript 只能保證它同時符合所有成員型別的共同屬性。例如:

let value: string | number;

此時 value 只允許使用 兩者共有 的方法(如 toString()),若直接呼叫 value.toFixed()(只屬於 number)編譯器會報錯。透過型別保護,我們可以在程式的某個分支裡 確認 value 真的是 number,從而安全地使用 toFixed()。


2. 內建型別保護方式

2.1 typeof

typeof 是最常見的保護手段,適用於原始型別(string、number、boolean、symbol、bigint、undefined):

function format(value: string | number) {
  if (typeof value === "string") {
    // 這裡 TypeScript 會把 value 窄化為 string
    return value.toUpperCase();
  } else {
    // 這裡 value 被視為 number
    return value.toFixed(2);
  }
}

2.2 instanceof

instanceof 用於判斷 物件是否為某個類別的實例,常見於自訂類別或內建建構子(Date、RegExp):

class Person {
  constructor(public name: string) {}
}

class Animal {
  constructor(public species: string) {}
}

function greet(entity: Person | Animal) {
  if (entity instanceof Person) {
    // entity 被窄化為 Person
    console.log(`Hello, ${entity.name}`);
  } else {
    // entity 為 Animal
    console.log(`Hello, ${entity.species}`);
  }
}

2.3 in 操作符

in 可以檢查 屬性是否存在於物件,常用於「結構型別」的保護:

type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };

function area(shape: Square | Circle) {
  if ("size" in shape) {
    // shape 被視為 Square
    return shape.size ** 2;
  } else {
    // shape 為 Circle
    return Math.PI * shape.radius ** 2;
  }
}

3. 自訂型別保護(User‑Defined Type Guard)

有時內建的保護不足以描述複雜的條件,這時可以 自行撰寫型別保護函式。自訂型別保護的寫法必須回傳 value is Type,這是 TypeScript 的 型別謂詞(type predicate)語法。

interface Dog {
  kind: "dog";
  bark(): void;
}
interface Cat {
  kind: "cat";
  meow(): void;
}

/** 判斷是否為 Dog */
function isDog(pet: Dog | Cat): pet is Dog {
  return pet.kind === "dog";
}

function talk(pet: Dog | Cat) {
  if (isDog(pet)) {
    // pet 被窄化為 Dog
    pet.bark();
  } else {
    // pet 為 Cat
    pet.meow();
  }
}

重點:型別謂詞必須放在函式的回傳型別位置,且 不能 使用 any、unknown 等會失去型別資訊的型別。


4. 判別聯合(Discriminated Unions)

當每個成員型別都有一個 唯一的字面值屬性(常稱 kind、type),TypeScript 能自動根據此屬性進行型別縮小,這叫 判別聯合。它是最簡潔、最安全的型別保護手法之一。

type Shape =
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "circle"; radius: number }
  | { kind: "triangle"; base: number; height: number };

function perimeter(s: Shape) {
  switch (s.kind) {
    case "rectangle":
      return 2 * (s.width + s.height);
    case "circle":
      return 2 * Math.PI * s.radius;
    case "triangle":
      return s.base + s.height + Math.hypot(s.base, s.height);
  }
}

在 switch 或 if 中檢查 s.kind 後,TypeScript 立即知道 s 的具體型別,不需要額外的 as 斷言。


5. 多層型別保護的範例

下面示範 結合 typeof、instanceof、自訂保護,處理一個可能是字串、Date、或自訂 LogEntry 物件的參數:

class LogEntry {
  constructor(public message: string, public level: "info" | "warn" | "error") {}
}

/** 判斷是否為 LogEntry */
function isLogEntry(v: unknown): v is LogEntry {
  return v instanceof LogEntry;
}

function process(input: string | Date | LogEntry) {
  if (typeof input === "string") {
    console.log("文字訊息:", input);
  } else if (input instanceof Date) {
    console.log("日期:", input.toISOString());
  } else if (isLogEntry(input)) {
    // 在這裡 input 已被窄化為 LogEntry
    console.log(`[${input.level.toUpperCase()}] ${input.message}`);
  } else {
    // 這條路徑理論上不會到,除非加入 unknown
    console.error("未知型別");
  }
}

常見陷阱與最佳實踐

陷阱 說明 建議的做法
忘記回傳 value is Type 自訂保護函式若只回傳 boolean,型別不會被縮小。 使用 型別謂詞 (param is SpecificType) 作為回傳型別。
過度使用 as 斷言 as 會直接告訴編譯器「我知道它是什麼」,容易掩蓋錯誤。 優先使用 內建保護 或 自訂型別保護,僅在確定安全且無法寫保護時才使用 as。
in 操作符檢查不存在的屬性 若屬性在某些型別中是可選的,in 仍會回傳 true,導致型別錯誤。 結合 hasOwnProperty 或 自訂保護 進一步驗證屬性型別。
判別聯合缺少唯一屬性 若每個成員沒有唯一的字面值屬性,TypeScript 無法自動縮小。 為每個子型別加入 kind / type 等 辨識屬性,或使用自訂保護。
在函式內部重新賦值破壞保護 保護後的變數若被重新賦值為寬鬆型別,後續的型別推斷會失效。 使用 const 或 let 並避免在保護區塊外改變變數型別。

最佳實踐:

  1. 先使用內建保護(typeof、instanceof、in),它們最直接且易於閱讀。
  2. 若內建保護不足,撰寫自訂型別保護函式,並以 型別謂詞 為返回型別。
  3. 儘量 採用判別聯合 的寫法,讓型別縮小成為語言層面的保證。
  4. 保持 函式的單一職責:型別保護只負責決定型別,真正的業務邏輯應該在保護之後的分支裡完成。
  5. 使用 嚴格模式 ("strict": true),這樣 TypeScript 會在缺少保護的情況下給出更明確的錯誤提示。

實際應用場景

場景 為何需要型別保護 範例概念
API 回傳的資料可能是成功或錯誤物件 回傳型別為 `Success Failure`,必須先判斷是哪一種才可安全存取屬性。
表單輸入值可能是字串或數字 依據不同型別執行不同的驗證規則(長度 vs 數值範圍)。 typeof value === "string" → 正則驗證;else → 數值比較。
混合型別的 UI 元件屬性 某個屬性可接受 string、ReactNode 或 (() => JSX.Element),渲染時必須根據型別分支。 if (typeof prop === "function") { ... } else if (React.isValidElement(prop)) { ... }
日誌系統接受多種訊息格式 訊息可能是純文字、錯誤物件或自訂 LogEntry,不同型別需要不同的處理流程。 參考上面的 process 範例。
第三方函式庫回傳 unknown unknown 必須先經過型別保護才能被使用,否則會失去型別安全。 if (isMyType(value)) { ... },其中 isMyType 為自訂保護。

在這些情況下,型別保護不僅避免執行時錯誤,同時讓 IDE 能夠提供更精確的自動完成與即時檢查,提升開發效率與程式碼可讀性。


總結

  • 型別保護 是 TypeScript 中讓編譯器在特定程式分支「窄化」變數型別的核心機制。
  • 內建的 typeof、instanceof、in 能快速處理原始型別、類別實例與結構型別。
  • 自訂型別保護(使用型別謂詞)允許我們針對更複雜的判斷條件提供安全的型別縮小。
  • 判別聯合(Discriminated Unions)是最簡潔、最具可維護性的做法,只要每個子型別都有唯一的字面值屬性。
  • 正確使用型別保護可避免 any/unknown 帶來的隱藏錯誤,提升程式的 安全性、可讀性與 IDE 支援。
  • 實務上,從 API 回傳、表單驗證、UI 屬性到日誌系統,都能透過型別保護寫出更穩定、易於維護的程式碼。

掌握型別保護,就是在 TypeScript 的型別系統裡,為程式碼加上一層「安全護欄」。只要多加練習、善用內建與自訂的保護手段,你的程式碼將會變得更健壯、更易於團隊協作。祝你在 TypeScript 的旅程中寫出乾淨、可靠的程式!