TypeScript 型別收窄 (Type Narrowing)

如果說 TypeScript 是你的程式碼保鑣,那「型別收窄 (Type Narrowing)」就是它的「邏輯推理能力」。

想像一下,你有一個變數可能是 string 也可能是 number。當你在程式碼中寫了 if 判斷,TypeScript 會像福爾摩斯一樣,根據你寫的邏輯,動態地推斷出在某個區塊內,這個變數一定是什麼型別。這個過程就叫做「收窄」。

最常見的收窄方式

typeof 檢查

這是最直覺的方法。

function padLeft(padding: number | string, input: string) {
  // 此時 padding 還是 number | string

  if (typeof padding === 'number') {
    // TypeScript 知道進入這個區塊,padding 一定是 number
    return ' '.repeat(padding) + input;
  }

  // TypeScript 知道如果能執行到這,padding 一定是 string
  // (因為如果是 number,上面就 return 走了)
  return padding + input;
}

真值收窄 (Truthiness Narrowing)

利用 JavaScript 在 if 中會將值轉為 boolean 的特性。

function printAll(strs: string | string[] | null) {
  if (strs) {
    // 這裡排除了 null (因為 null 是 falsy)
    // 但strs 仍可能是 string 或 string[]
    if (typeof strs === 'object') {
      // 被收窄為 string[] (因為 string 不是 object)
      for (const s of strs) {
        console.log(s);
      }
    } else {
      // 被收窄為 string
      console.log(strs);
    }
  }
}

小心陷阱:空字串 "" 和數字 0 在 JavaScript 中也是 falsy。如果你只是想排除 null,直接用 != null 會更安全。

相等性收窄 (Equality Narrowing)

使用 ===!==

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // 如果 x 等於 y,那它們一定都是 string
    // 因為 number 和 boolean 不可能全等
    x.toUpperCase();
    y.toUpperCase();
  }
}

in 運算子收窄

用來檢查物件中是否有某個屬性。

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    // 有 swim 的一定是 Fish
    animal.swim();
  } else {
    // 否則那就是 Bird
    animal.fly();
  }
}

instanceof 收窄

檢查是否為某個類別的實例,常用於處理 Date 或自訂類別。

function logValue(x: Date | string) {
  if (x instanceof Date) {
    // x 是 Date
    console.log(x.toUTCString());
  } else {
    // x 是 string
    console.log(x.toUpperCase());
  }
}

控制流程分析 (Control Flow Analysis)

TypeScript 的分析能力非常強大,它會追蹤程式的執行路徑。

function example() {
  let x: string | number | boolean;

  x = Math.random() < 0.5;
  // 現在 x 是 boolean

  if (Math.random() < 0.5) {
    x = 'hello';
    // 現在 x 是 string
  } else {
    x = 100;
    // 現在 x 是 number
  }

  return x; // 回傳 string | number
}

Discriminated Unions (可辨識聯合)

這是處理複雜型別最優雅的方式。我們給每個介面一個共同的欄位(通常叫 kindtype)來當作標籤。

interface Circle {
  kind: 'circle'; // 字面量型別
  radius: number;
}

interface Square {
  kind: 'square'; // 字面量型別
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
  // 透過檢查共同欄位 kind
  switch (shape.kind) {
    case 'circle':
      // shape 自動收窄為 Circle
      return Math.PI * shape.radius ** 2;
    case 'square':
      // shape 自動收窄為 Square
      return shape.sideLength ** 2;
  }
}

總結

型別收窄是 TypeScript 讓我們寫出既安全又像原生 JavaScript 一樣靈活的程式碼的關鍵。

你不必總是顯式地轉型 (as),只要你的邏輯判斷是合理的(例如用了 typeof, if, switch),TypeScript 通常都能理解你想做什麼,並自動幫你把型別變精確。