本文 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 是最常見的保護手段,適用於原始型別(stringnumberbooleansymbolbigintundefined):

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 用於判斷 物件是否為某個類別的實例,常見於自訂類別或內建建構子(DateRegExp):

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();
  }
}

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


4. 判別聯合(Discriminated Unions)

當每個成員型別都有一個 唯一的字面值屬性(常稱 kindtype),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);
  }
}

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


5. 多層型別保護的範例

下面示範 結合 typeofinstanceof、自訂保護,處理一個可能是字串、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辨識屬性,或使用自訂保護。
在函式內部重新賦值破壞保護 保護後的變數若被重新賦值為寬鬆型別,後續的型別推斷會失效。 使用 constlet 並避免在保護區塊外改變變數型別。

最佳實踐

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

實際應用場景

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

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


總結

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

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