本文 AI 產出,尚未審核

TypeScript – 型別推論與型別保護(Union Narrowing)


簡介

在日常的 JavaScript 開發中,我們常會遇到「一個變數可能是多種型別」的情況,例如 API 回傳的資料可能是 stringnull,或是函式參數可以接受 number | string
TypeScript 透過 Union 型別 讓我們可以明確描述這類「或」的關係,但僅僅寫出 type T = A | B 並不足以讓編譯器在使用時自動判斷到底是哪一個型別。

Union narrowing(聯合型別的收窄)則是 TypeScript 的型別保護機制之一,能在程式執行流程中根據條件判斷把寬泛的 Union 型別「縮小」成更具體的單一型別。掌握這項技巧,能讓程式碼在編譯階段就捕捉到潛在的錯誤,同時保留良好的開發體驗(自動完成、型別提示)。


核心概念

1. 什麼是 Union Narrowing?

Union narrowing 指的是 在程式的控制流程裡,透過條件判斷(if、switch、in、typeof、instanceof 等)把 A | B | C 這樣的聯合型別縮減為單一的 ABC
縮窄後,編譯器會把變數的型別視為被縮窄的那一個,從而允許存取該型別特有的屬性或方法。

function foo(x: string | number) {
  // 這裡 x 仍是 string | number
  if (typeof x === "string") {
    // 這裡 x 被縮窄為 string
    console.log(x.toUpperCase());
  } else {
    // 這裡 x 被縮窄為 number
    console.log(x.toFixed(2));
  }
}

2. 常見的型別保護方式

保護方式 說明 範例
typeof 用於基本型別(stringnumberbooleansymbolbigintfunction if (typeof v === "number") …
instanceof 判斷物件是否為某個建構子(class)的實例 if (obj instanceof Date) …
in 檢查屬性是否存在於物件上 if ("length" in arr) …
自訂型別保護(type guard) 使用返回值類型為 x is Y 的函式 function isUser(o: any): o is User { … }
Array.isArray 判斷是否為陣列(特殊的型別保護) if (Array.isArray(v)) …

3. 範例一:基本型別的縮窄

function format(value: string | number | boolean) {
  if (typeof value === "string") {
    // value 為 string
    return `"${value}"`;
  }
  if (typeof value === "number") {
    // value 為 number
    return value.toLocaleString();
  }
  // 只剩下 boolean
  return value ? "YES" : "NO";
}
  • 重點typeof 能直接把 Union 中的基本型別分離,縮窄後即可安全使用該型別的方法。

4. 範例二:in 关键字檢查屬性

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

function area(shape: Circle | Square) {
  if ("radius" in shape) {
    // shape 被視為 Circle
    return Math.PI * shape.radius ** 2;
  } else {
    // shape 被視為 Square
    return shape.side ** 2;
  }
}
  • 技巧in 只要檢查屬名是否存在,就能把聯合型別依屬性分割。這在「同一屬性名稱不會同時出現在不同型別」時特別好用。

5. 範例三:自訂型別保護(type guard)

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

function isDog(pet: Pet): pet is Dog {
  return pet.kind === "dog";
}

function talk(pet: Pet) {
  if (isDog(pet)) {
    // pet 被縮窄為 Dog
    pet.bark();
  } else {
    // pet 被縮窄為 Cat
    pet.meow();
  }
}
  • 說明:自訂的 isDog 函式回傳型別謂詞 pet is Dog,讓 TypeScript 在 if (isDog(pet)) 裡自動把 pet 縮窄為 Dog

6. 範例四:instanceof 與類別

class Bird {
  fly() { console.log("fly"); }
}
class Fish {
  swim() { console.log("swim"); }
}
type Animal = Bird | Fish;

function move(a: Animal) {
  if (a instanceof Bird) {
    a.fly();      // a 為 Bird
  } else {
    a.swim();     // a 為 Fish
  }
}
  • 注意instanceof 只能用在有實體建構子的類別,對於純介面(interface)無效。

7. 範例五:結合 Array.isArray 與聯合型別

function flatten(input: number | number[] | number[][]) {
  if (Array.isArray(input)) {
    // input 為 number[] 或 number[][]
    return input.flat(2);
  }
  // input 為單一 number
  return [input];
}
  • 重點Array.isArray 是檢查「是否為陣列」的唯一型別保護,適用於任意深度的陣列型別。

常見陷阱與最佳實踐

陷阱 說明 解法
屬性名稱重疊 若兩個型別都擁有相同屬名,in 無法正確縮窄。 使用 kind 標籤或自訂 type guard。
typeof 無法辨別 null typeof null 會回傳 "object",容易與其他物件混淆。 先檢查 value === null 再使用 typeof
instanceof 只對 class 有效 介面或型別別名無法使用 instanceof 改用 in 或自訂型別保護。
過度依賴 any 把 Union 變成 any 會失去縮窄的好處。 盡量保留具體型別,使用 unknown 搭配 type guard。
忘記返回 never switch 完整性檢查時,未處理的情況會回傳 never,導致錯誤被忽略。 default 中拋出錯誤或回傳 never 以保證完整性。

最佳實踐

  1. 使用 discriminated unions(具標籤的聯合型別):在每個型別加入唯一的 kindtype 屬性,讓 switch / if 判斷更直觀。
  2. 盡量寫自訂 type guard:可重用且易於測試,尤其在大型專案中。
  3. 保持型別的可讀性:過於複雜的 Union 會降低可維護性,必要時可拆分為多個小型 Union。
  4. 結合 never 進行完整性檢查:在 switchdefault 中使用 assertNever(value),確保未來加入新型別時會得到編譯錯誤提醒。
function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}

實際應用場景

  1. API 回傳的多型別結果

    type ApiResponse = { status: "ok"; data: User } |
                      { status: "error"; error: string };
    
    function handle(resp: ApiResponse) {
      if (resp.status === "ok") {
        // resp 被縮窄為 { status: "ok"; data: User }
        console.log(resp.data.name);
      } else {
        // resp 為 error 形態
        console.error(resp.error);
      }
    }
    
  2. 表單欄位的多種輸入型別

    type InputValue = string | number | Date;
    
    function formatInput(v: InputValue) {
      if (v instanceof Date) return v.toISOString();
      if (typeof v === "number") return v.toLocaleString();
      return v.trim();
    }
    
  3. React 組件的 Props 多型別

    type ButtonProps = { type: "primary"; onClick: () => void } |
                       { type: "link"; href: string };
    
    const Button = (props: ButtonProps) => {
      if (props.type === "primary") {
        return <button onClick={props.onClick}>Primary</button>;
      }
      return <a href={props.href}>Link</a>;
    };
    
  4. 資料處理管線的不同階段
    在資料流中,每個階段可能傳遞 string | Buffer | null,使用 if (value !== null && typeof value === "string") 能安全地執行文字處理。


總結

  • Union narrowing 是 TypeScript 型別保護的核心機制,讓我們可以在執行流程中把寬泛的聯合型別縮減為具體型別。
  • 常見的縮窄手段包括 typeofinstanceofinArray.isArray 以及 自訂 type guard
  • 正確使用縮窄不僅能提升編譯時安全性,也能讓 IDE 提供更精準的自動完成與錯誤提示。
  • 在實務開發中,建議採用 discriminated unions、撰寫可重用的 type guard,並配合 never 完整性檢查,避免常見的陷阱。

掌握了 Union narrowing,您就能在 TypeScript 專案裡寫出更安全可讀易維護的程式碼,並充分發揮靜態型別的威力。祝開發順利!