本文 AI 產出,尚未審核

TypeScript – 型別推論與型別保護(Type Inference & Narrowing)

主題:typeof 型別保護


簡介

在 JavaScript 的執行階段,typeof 是最常見的「型別偵測」工具;而在 TypeScript 中,它同時也是 型別保護(type guard) 的核心之一。透過 typeof,編譯器能在條件分支裡 自動縮小(narrow) 變數的型別,使得後續的程式碼取得正確的型別資訊,減少 any 或手動斷言的使用。

對於 初學者,了解 typeof 如何與 TypeScript 的型別推論結合,是寫出安全、可維護程式的第一步。對 中級開發者,則能利用 typeof 進行更精細的型別守護,搭配自訂型別保護或泛型,打造彈性十足的函式庫。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整呈現 typeof 型別保護的使用方式與實務價值。


核心概念

1. typeof 在 JavaScript 與 TypeScript 的差異

語言 回傳值 主要用途
JavaScript "number""string""boolean""undefined""object""function""symbol""bigint" 執行時檢查
TypeScript 同上,但 同時提供型別縮小 編譯時推斷、減少 any

重點:在 TypeScript 中,typeof 只在 條件式ifswitch、三元運算子)內才會觸發型別縮小。離開條件分支後,變數會恢復原本的寬鬆型別。


2. 基本型別保護:primitive 類型

function double(value: number | string) {
  // 依據 typeof 進行型別保護
  if (typeof value === "number") {
    // 這裡 value 被縮小為 number
    return value * 2;
  } else {
    // 這裡 value 被縮小為 string
    return value + value;
  }
}

// 測試
console.log(double(10));   // 20
console.log(double("Hi")); // HiHi

說明

  • value 原本是聯合型別 number | string
  • if (typeof value === "number") 成立時,編譯器 自動把 value 視為 number,因此可以直接使用算術運算。
  • 反之,else 分支則被視為 string,允許字串相加。

3. 與 nullundefined 搭配使用

nullundefinedtypeof 中分別回傳 "object"null)與 "undefined",因此在保護它們時常需要額外的檢查。

function format(input: string | null | undefined) {
  if (typeof input === "string") {
    return input.trim();
  }

  // 此時 input 仍可能是 null 或 undefined
  if (input == null) { // 同時檢查 null 與 undefined
    return "";
  }

  // 這裡永遠不會執行到
  return "unreachable";
}

說明

  • typeof input === "string" 只保護字串。
  • input == null(使用寬鬆相等)同時捕捉 nullundefined,這是 常見的型別保護技巧

4. 與函式結合的 typeof 保護

typeof 也能辨識 函式,讓我們在接受多型參數時,安全地呼叫它。

type Callback = ((msg: string) => void) | undefined;

function greet(name: string, cb?: Callback) {
  console.log(`Hello, ${name}!`);

  if (typeof cb === "function") {
    // cb 被縮小為 (msg: string) => void
    cb(`Welcome, ${name}`);
  }
}

說明

  • cb 可能是 undefined,所以先用 typeof cb === "function" 確認。
  • 這樣做比 if (cb) 更精確,因為 cb 可能是其他 truthy 值(如 0"")在未來的型別變更中造成誤判。

5. typeof自訂型別保護 的結合

雖然 typeof 只能辨識 primitive 類型與函式,但我們可以 在自訂型別保護裡使用 typeof,讓保護更具彈性。

interface Person {
  name: string;
  age: number;
}

function isPerson(value: any): value is Person {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof value.name === "string" &&
    typeof value.age === "number"
  );
}

// 使用
function printInfo(data: Person | string) {
  if (isPerson(data)) {
    // data 被縮小為 Person
    console.log(`${data.name} (${data.age} 歲)`);
  } else {
    // data 為 string
    console.log(`訊息:${data}`);
  }
}

說明

  • isPerson 使用 typeof 檢查 objectstringnumber,同時返回 型別謂詞 (value is Person)。
  • printInfo 中,if (isPerson(data)) 讓 TypeScript 把 data 縮小為 Person,即使 Person 本身是一個自訂介面。

常見陷阱與最佳實踐

陷阱 可能的錯誤 正確寫法
null 當作 object typeof null === "object" 會誤判為有效物件 同時檢查 value !== null
忽略 bigintsymbol 只檢查 "number""string",導致其他 primitive 被忽略 加入 typeof v === "bigint""symbol" 的分支
使用寬鬆相等 == 判斷 null 可能在未預期的情況下把 0false 誤判 建議使用 `value === null
在非條件語句中使用 typeof 型別不會被縮小,仍是原始聯合型別 只在 ifelse ifswitch、三元運算子等分支內使用
typeof 結果寫成字面量型別 if (typeof x === "number") 之後仍被視為 any(舊版 TS) 確保 TypeScript 版本 >= 2.0,或使用 as const 斷言結果字串

最佳實踐

  1. 明確列出所有 primitive

    if (typeof v === "string") { … }
    else if (typeof v === "number") { … }
    else if (typeof v === "boolean") { … }
    else if (typeof v === "bigint") { … }
    else if (typeof v === "symbol") { … }
    else if (typeof v === "undefined") { … }
    else if (typeof v === "function") { … }
    else if (v === null) { … }
    
  2. 結合 Array.isArray 判斷陣列typeof 只能回傳 "object"

    if (Array.isArray(value)) { … }
    
  3. 將保護邏輯抽成函式,提升可讀性與重用性(如 isPerson 範例)。

  4. 使用 never 讓未處理的型別在編譯時錯誤

    function exhaustiveCheck(x: never): never {
      throw new Error(`未處理的型別: ${x}`);
    }
    
  5. 保持 TypeScript 版本更新,新版會更好地支援 typeof 型別保護與型別縮小。


實際應用場景

1. API 回傳的多型資料

假設後端回傳的資料可能是 字串(錯誤訊息)或 物件(成功結果):

type ApiResponse = { data: string[] } | string;

function handleResponse(res: ApiResponse) {
  if (typeof res === "string") {
    console.error(`API 錯誤:${res}`);
    return;
  }

  // 這裡 res 被縮小為 { data: string[] }
  console.log("取得資料:", res.data);
}

2. 事件處理函式

瀏覽器的 Event 物件在某些情況下會是 KeyboardEventMouseEvent… 使用 typeof 判斷 函式型別,避免 null 造成的 Runtime Error:

function addListener(
  el: HTMLElement,
  type: string,
  handler?: ((e: Event) => void) | null
) {
  if (typeof handler === "function") {
    el.addEventListener(type, handler);
  } else {
    console.warn("未提供有效的事件處理函式");
  }
}

3. 多型函式庫的參數驗證

開發一個通用的 深拷貝 函式,需根據參數的 primitive 類型決定拷貝方式:

function deepClone<T>(value: T): T {
  if (typeof value !== "object" || value === null) {
    // 基本型別或 null,直接回傳
    return value;
  }

  if (Array.isArray(value)) {
    return value.map(item => deepClone(item)) as unknown as T;
  }

  const result: any = {};
  for (const key in value) {
    result[key] = deepClone((value as any)[key]);
  }
  return result;
}

總結

  • typeofTypeScript 中不僅是執行時的型別偵測,更是 型別保護 的核心工具,能讓編譯器在條件分支裡自動 縮小變數型別
  • 透過 typeof 結合 null 判斷、函式檢查、陣列檢測,我們可以安全地處理 number | string | undefined | null 等聯合型別,避免 any 的濫用。
  • 常見陷阱包括 null 被視為 "object"、遺漏 bigint/symbol、在非條件語句中使用 typeof 等;只要遵循 「列舉所有 primitive」「在條件分支內使用」 的原則,就能寫出可靠的程式碼。
  • 在實務上,typeof 型別保護常見於 API 回傳處理、事件監聽、通用函式庫 等情境,配合自訂型別保護(type predicate)可進一步提升程式的可讀性與可維護性。

掌握 typeof 的型別保護技巧,等於在 TypeScript 的型別系統上多了一層安全防護,讓開發者能以更少的斷言、更自然的程式碼,寫出 安全、可預測、易維護 的 JavaScript/TypeScript 應用。祝你在 TypeScript 的旅程中,玩得開心、寫得順手!