本文 AI 產出,尚未審核

TypeScript • 型別推論與型別保護(Control Flow Analysis)


簡介

在 JavaScript 世界裡,變數的型別是「動態」的,程式在執行時才會決定到底是字串、數字或是物件。TypeScript 為了在編譯階段就捕捉錯誤,引入了型別推論(type inference)型別保護(type narrowing)。其中最核心的機制就是 Control Flow Analysis(控制流程分析)——編譯器會根據程式的執行路徑,動態推斷變數的可能型別,並在適當的時候「縮窄」它們。

為什麼這麼重要?

  1. 提升開發效率:不必手動為每個變數寫完整的型別宣告,編譯器會自動幫你補完。
  2. 減少執行時錯誤:在條件分支、switchif 內部,TypeScript 能保證你只對符合條件的型別呼叫屬性或方法。
  3. 增進程式可讀性:控制流程分析讓程式的意圖更清晰,未來維護或重構時也不易出現型別不一致的問題。

接下來,我們會一步步拆解「控制流程分析」的運作原理,並透過實務範例說明如何在日常開發中善加利用。


核心概念

1. 基本型別推論

當變數在宣告時即被賦值,TypeScript 會依照賦值的字面值推斷型別。

let count = 0;          // 推論為 number
const name = "Alice";   // 推論為 "Alice"(字面量類型)
let flag = true;        // 推論為 boolean

注意const 會保留字面量型別(例如 "Alice"),而 let 會退化成更寬鬆的基本型別(string)。

2. 控制流程分析的工作原理

控制流程分析會在 程式的控制流(if、else、switch、while、for、try/catch…) 中,根據條件的結果「縮窄」變數的型別。簡單來說,編譯器會把 「可能的型別集合」 逐步縮小,直到只能剩下單一型別或是更精確的聯合型別。

2.1 if / else 之間的縮窄

function printLength(value: string | number) {
  if (typeof value === "string") {
    // 這裡 value 已被縮窄為 string
    console.log(value.length);
  } else {
    // 這裡 value 被縮窄為 number
    console.log(value.toFixed(2));
  }
}
  • typeof value === "string"型別保護(type guard),讓編譯器在 if 區塊內把 value 視為 string,在 else 區塊視為 number
  • 若在 if 內直接寫 value.toFixed(2),編譯器會報錯,因為此時 value 仍被視為 string | number

2.2 switch 內的縮窄

type Shape = 
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rect"; width: number; height: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle":
      // s 被縮窄為 { kind: "circle"; radius: number }
      return Math.PI * s.radius ** 2;
    case "square":
      // s 被縮窄為 { kind: "square"; side: number }
      return s.side ** 2;
    case "rect":
      // s 被縮窄為 { kind: "rect"; width: number; height: number }
      return s.width * s.height;
  }
}
  • switch 會自動把 s 的型別根據 case 中的字面值「分支」縮窄。
  • 只要 case 內的屬性在對應的型別中存在,編譯器就會正確推斷,避免手動使用 as 斷言。

2.3 迴圈與條件判斷

function filterNumbers(arr: (string | number)[]): number[] {
  const result: number[] = [];
  for (const item of arr) {
    if (typeof item === "number") {
      // item 在此區塊內被縮窄為 number
      result.push(item);
    }
  }
  return result;
}
  • for...of 迴圈中,item 的型別同樣會受到 if 條件的縮窄影響。
  • 這讓我們可以安全地把 item 推入 number[],而不會因為「可能是字串」而產生錯誤。

2.4 真值檢查(Truthiness Check)

function greet(name?: string) {
  if (name) {
    // name 在此被視為 string(非 undefined、null、空字串)
    console.log(`Hello, ${name}!`);
  } else {
    console.log("Hello, guest!");
  }
}
  • TypeScript 把 if (name) 視為「排除 undefinednull""」的保護,讓 nameif 內部被認定為 string
  • 這與 JavaScript 的真值概念一致,但在型別層面提供了安全保證。

3. 用戶自訂的型別保護(User‑Defined Type Guards)

有時候內建的 typeofinstanceof 不足,我們可以自行寫函式返回 類型謂詞(type predicate):

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

function isCat(pet: Cat | Dog): pet is Cat {
  return pet.kind === "cat";
}

function talk(pet: Cat | Dog) {
  if (isCat(pet)) {
    // pet 被縮窄為 Cat
    pet.meow();
  } else {
    // pet 被縮窄為 Dog
    pet.bark();
  }
}
  • pet is Cat類型謂詞,告訴編譯器「只要 isCat 回傳 truepet 就一定是 Cat」。
  • 這種方式非常適合在大型代碼庫中建立可重用的型別保護函式。

4. 交叉型別與縮窄的限制

type A = { a: number; common: string };
type B = { b: boolean; common: string };
type AB = A | B;

function getCommon(x: AB) {
  // 直接存取 common 仍然安全,因為兩個型別都有此屬性
  return x.common;
}
  • 若所有聯合成員都具備同名屬性,縮窄不會改變其可用性。
  • 但若屬性在部分成員缺失,必須先做保護才能安全存取:
function getA(x: AB) {
  if ("a" in x) {
    // x 被縮窄為 A
    return x.a;
  }
  // 這裡 x 為 B,沒有 a 屬性
}

程式碼範例

以下提供 5 個實用範例,展示控制流程分析在日常開發中的典型應用。每段程式碼皆附有說明註解。

範例 1:API 回傳的多型別結果

type ApiResponse =
  | { status: "ok"; data: string[] }
  | { status: "error"; error: Error };

function handleResponse(res: ApiResponse) {
  if (res.status === "ok") {
    // ✅ res 被縮窄為 { status: "ok"; data: string[] }
    console.log("取得資料:", res.data.join(", "));
  } else {
    // ✅ res 被縮窄為 { status: "error"; error: Error }
    console.error("發生錯誤:", res.error.message);
  }
}

實務意義:在前端與後端溝通時,常見「成功」與「失敗」的不同結構,控制流程分析讓我們不必手動斷言,減少錯誤。

範例 2:表單欄位的動態驗證

type Field =
  | { type: "text"; value: string }
  | { type: "number"; value: number }
  | { type: "checkbox"; checked: boolean };

function validate(field: Field) {
  switch (field.type) {
    case "text":
      // field 被縮窄為 { type: "text"; value: string }
      return field.value.trim().length > 0;
    case "number":
      // field 被縮窄為 { type: "number"; value: number }
      return !isNaN(field.value);
    case "checkbox":
      // field 被縮窄為 { type: "checkbox"; checked: boolean }
      return field.checked === true;
  }
}

實務意義:表單元件往往有多種型別,使用 switch 讓每個分支只關注自己需要的屬性,程式碼更易讀且安全。

範例 3:自訂型別保護檢查 DOM 元素

function isHTMLInputElement(el: Element): el is HTMLInputElement {
  return el instanceof HTMLInputElement;
}

function getInputValue(el: Element): string | null {
  if (isHTMLInputElement(el)) {
    // el 被縮窄為 HTMLInputElement
    return el.value;
  }
  return null;
}

實務意義:在操作瀏覽器 DOM 時,常需要先確認元素類型,這裡的自訂型別保護讓 TypeScript 能正確推斷 value 屬性的存在。

範例 4:處理 nullundefined 的安全寫法

function getLength(s?: string | null): number {
  // 先排除 null、undefined、空字串
  if (!s) return 0;
  // 此時 s 被縮窄為 string
  return s.length;
}

實務意義:API 回傳的字串欄位常會是 nullundefined,使用真值檢查可一次排除多種「不存在」的情況。

範例 5:從混合陣列過濾出特定型別

type Primitive = string | number | boolean;

function filterPrimitives(arr: any[]): Primitive[] {
  const out: Primitive[] = [];
  for (const v of arr) {
    if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
      // v 在此被縮窄為 Primitive
      out.push(v);
    }
  }
  return out;
}

實務意義:在處理 JSON 或外部資料時,常需要把雜湊資料過濾成特定型別,控制流程分析讓 push 的型別安全性自動得到保障。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記 break/returnswitch 雖然 TypeScript 仍會縮窄,但執行時會跌入下一個 case,可能導致不預期的行為。 在每個 case 內使用 returnbreakthrow 結束分支。
使用 any 破壞推論 一旦變數被指定為 any,控制流程分析無法再提供型別安全。 盡量避免 any,改用 unknown 並在需要時手動保護。
過度依賴 as 斷言 as 會告訴編譯器「我知道這是什麼」,但若斷言錯誤,執行時會出錯。 優先使用 typeofinstanceof 或自訂型別保護,僅在確定安全時才使用 as
聯合型別的共同屬性 只因所有成員都有某屬性,編譯器不會縮窄至單一型別,仍保留聯合型別。 若需要更精細的型別資訊,使用 in 檢查或自訂型別保護。
null/undefined 的真值檢查 if (value) 會排除 0false 等「假值」,若這些值在你的邏輯中是合法的,會造成誤判。 針對 null/undefined 使用 value != null 或明確的類型比較。

最佳實踐

  1. 讓編譯器自行推論:除非有特殊需求,盡量省略顯式的型別註記,讓 TypeScript 透過控制流程自動推斷。
  2. 使用內建型別保護typeofinstanceofin、真值檢查是最直接且效能最好的保護方式。
  3. 建立可重用的型別保護函式:對於複雜的判斷(如深層物件結構),寫成 isXxx 函式,提升可讀性與一致性。
  4. switch 中列舉所有可能的 case:若使用 never 來捕捉未處理的型別,可在未來擴充聯合型別時得到編譯錯誤提醒。
function exhaustiveCheck(x: never): never {
  throw new Error(`未處理的值: ${x}`);
}

// 範例
function foo(v: "a" | "b") {
  switch (v) {
    case "a":
      // ...
      break;
    case "b":
      // ...
      break;
    default:
      exhaustiveCheck(v); // 編譯時若新增 "c" 會報錯
  }
}

實際應用場景

1. 前端表單驗證框架

在大型表單(多步驟、動態欄位)中,欄位的型別往往在執行時才決定。透過控制流程分析,我們可以寫出一套 型別安全的驗證函式庫,每個驗證器只需要關注自己能處理的型別,其他分支自動被排除。

2. Redux / NgRx 狀態管理

狀態物件通常是 聯合型別(例如 Loading | Success<T> | Failure),在 reducer 或 selector 中使用 if (state.status === "Success") 時,TypeScript 立即縮窄至 Success<T>,讓開發者無需手動斷言即可安全存取 state.payload

3. Node.js 後端 API 參數驗證

在 Express、Koa 等框架中,req.body 常是 any。透過自訂型別保護(例如 isCreateUserDto(req.body))),我們能在進入業務邏輯前把 req.body 縮窄為明確的 DTO(Data Transfer Object),同時避免 any 帶來的隱藏錯誤。

4. 跨平台程式庫(如 React Native)

平台差異導致某些 API 僅在 iOS 或 Android 上可用。使用 if (Platform.OS === "ios") 時,編譯器會把相關變數縮窄,讓只能在 iOS 上使用的型別(如 IOSSpecificProp) 不會在 Android 分支中被誤用。


總結

  • 控制流程分析 是 TypeScript 型別系統的核心機制,讓編譯器根據程式的執行路徑自動推斷與縮窄型別。
  • 透過 if/elseswitch、迴圈、真值檢查以及 自訂型別保護,我們可以在不寫冗長型別宣告的前提下,仍然得到完整的型別安全。
  • 正確運用這些特性,不僅能減少執行時錯誤,還能提升程式可讀性與維護性。
  • 在實務開發中,從 API 回傳結果、表單驗證、狀態管理到跨平台差異,都能看到控制流程分析的身影。只要遵守最佳實踐、避免 any 與過度斷言,我們就能充分發揮 TypeScript 的型別力量。

關鍵一句話讓 TypeScript 的控制流程分析為你自動「看門」——在每一次條件分支裡,保證變數只會是它被允許的型別。