本文 AI 產出,尚未審核

TypeScript 課程:型別推論與型別保護 – in 運算子型別保護


簡介

在 JavaScript 中,物件的屬性往往是動態決定的,這讓開發者在編寫大型程式時容易遭遇 執行時錯誤(例如存取不存在的屬性)。TypeScript 透過 型別保護(type guarding),在編譯階段就能幫助我們排除這類問題。

in 運算子是 TypeScript 提供的 型別保護 手段之一,它可以在 條件分支 中縮窄(narrow)變數的型別,讓編譯器知道某個屬性一定存在。掌握 in 的使用方式,不僅能提升程式的安全性,還能讓 IDE 提供更精確的自動完成與錯誤提示,對於 初學者中階開發者 都是必備的技巧。

本文將以 實務範例 為主軸,說明 in 運算子如何在型別推論與型別保護中發揮作用,並探討常見陷阱與最佳實踐,最後提供幾個在真實專案中常見的應用情境。


核心概念

1. in 運算子是什麼?

in 是 JavaScript 的原生運算子,用來檢查 屬性名稱 是否存在於物件或其原型鏈上。

const obj = { name: "Alice", age: 30 };
"name" in obj;   // true
"email" in obj;  // false

在 TypeScript 中,in 同時具備 型別保護 的功能:當條件判斷為 true 時,編譯器會把左側的屬性視為 必定存在,進而縮窄變數的聯合型別 (union type)。

2. 為什麼需要 in 來做型別保護?

假設我們有兩個介面:

interface Dog {
  kind: "dog";
  bark(): void;
  weight: number;
}

interface Bird {
  kind: "bird";
  fly(): void;
  wingSpan: number;
}

如果有一個參數的型別是 Dog | Bird,直接呼叫 bark() 會產生錯誤,因為 編譯器無法確定 真正的型別。使用 in 可以這樣做:

function handleAnimal(animal: Dog | Bird) {
  if ("bark" in animal) {
    // 在這個區塊內,animal 被縮窄為 Dog
    animal.bark();
    console.log(`Weight: ${animal.weight}`);
  } else {
    // 這裡則是 Bird
    animal.fly();
    console.log(`Wing span: ${animal.wingSpan}`);
  }
}

當條件 ("bark" in animal)true 時,TypeScript 會把 animal 的型別 Dog | Bird 縮窄成 Dog,因此可以安全地存取 barkweight 等屬性。

3. inkeyof 的結合

如果你想要寫一個 通用的型別保護函式,可以利用 keyof 取得所有可能的屬性鍵,然後配合 in

function hasKey<O extends object, K extends PropertyKey>(
  obj: O,
  key: K
): obj is O & Record<K, unknown> {
  return key in obj;
}

// 使用範例
type Person = { name: string; age: number };
type Car = { brand: string; speed: number };

function logInfo(item: Person | Car) {
  if (hasKey(item, "name")) {
    // item 被視為 Person
    console.log(`Person: ${item.name}, ${item.age}`);
  } else {
    // item 被視為 Car
    console.log(`Car: ${item.brand}, ${item.speed}km/h`);
  }
}

這裡的 hasKey 回傳 型別謂詞 (obj is O & Record<K, unknown>),讓 TypeScript 能在呼叫處自動完成型別縮窄。

4. in 也能保護 字串字面值聯合型別

當你有一個字串聯合型別,想要根據字串的值分支時,in 同樣適用:

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

function area(s: Shape): number {
  if ("radius" in s) {
    // s 為 circle
    return Math.PI * s.radius ** 2;
  }
  if ("side" in s) {
    // s 為 square
    return s.side ** 2;
  }
  // 只能是 rectangle
  return s.width * s.height;
}

此技巧比使用 switch (s.kind) 更直觀,尤其當屬性名稱本身就能唯一辨識型別時。

5. in可選屬性 (optional properties)

對於具有可選屬性的介面,in 能協助檢查屬性是否真的被賦值:

interface Config {
  url: string;
  timeout?: number; // 可選
}

function fetchData(cfg: Config) {
  const opts: RequestInit = { method: "GET" };
  if ("timeout" in cfg) {
    // timeout 已被提供
    opts.signal = AbortSignal.timeout(cfg.timeout!);
  }
  // ...
}

注意:即使屬性值是 undefined,只要屬性鍵在物件上(即使值為 undefined),in 仍會回傳 true。因此在可選屬性的情境下,若需要判斷「是否真的有值」應再檢查 !== undefined


程式碼範例

範例 1:基本的 in 型別保護

interface Cat {
  kind: "cat";
  meow(): void;
  lives: number;
}
interface Fish {
  kind: "fish";
  swim(): void;
  waterTemp: number;
}

function interact(pet: Cat | Fish) {
  if ("meow" in pet) {
    // pet 被縮窄為 Cat
    pet.meow();
    console.log(`Lives left: ${pet.lives}`);
  } else {
    // pet 為 Fish
    pet.swim();
    console.log(`Water temperature: ${pet.waterTemp}°C`);
  }
}

重點"meow" in pet 讓編譯器知道 pet 必定是 Cat,因此可以安全呼叫 meow()lives


範例 2:結合 keyof 實作通用型別保護

function hasProp<O extends object, K extends PropertyKey>(
  obj: O,
  prop: K
): obj is O & Record<K, unknown> {
  return prop in obj;
}

type ApiResponse = 
  | { status: 200; data: string }
  | { status: 404; error: string }
  | { status: 500; retryAfter: number };

function handleResponse(res: ApiResponse) {
  if (hasProp(res, "data")) {
    console.log(`Success: ${res.data}`);
  } else if (hasProp(res, "error")) {
    console.warn(`Not found: ${res.error}`);
  } else {
    console.error(`Server error, retry after ${res.retryAfter}s`);
  }
}

技巧hasProp 透過 型別謂詞 讓呼叫端直接得到縮窄後的型別,省去重複寫 if ("data" in res) 的麻煩。


範例 3:使用 in 判斷可選屬性

interface UserOptions {
  username: string;
  email?: string;          // 可選
  isAdmin?: boolean;       // 可選
}

function createUser(opts: UserOptions) {
  const user = { id: Date.now(), ...opts };

  if ("email" in opts) {
    // email 屬性確實存在(即使值是 undefined)
    console.log(`Send welcome email to ${opts.email}`);
  }

  // 若要檢查值是否真的有提供
  if (opts.email !== undefined) {
    console.log(`Valid email: ${opts.email}`);
  }

  // 判斷管理員權限
  const isAdmin = "isAdmin" in opts ? !!opts.isAdmin : false;
  console.log(`Is admin? ${isAdmin}`);
}

注意"email" in opts 只保證屬性鍵存在,若要確保值非 undefined,仍需額外檢查。


範例 4:in 與字串聯合型別的結合

type Notification =
  | { type: "email"; to: string; subject: string }
  | { type: "sms"; phone: string; message: string }
  | { type: "push"; deviceId: string; payload: any };

function send(notif: Notification) {
  if ("to" in notif) {
    // email
    console.log(`Sending email to ${notif.to}: ${notif.subject}`);
  } else if ("phone" in notif) {
    // sms
    console.log(`Sending SMS to ${notif.phone}: ${notif.message}`);
  } else {
    // push
    console.log(`Push to ${notif.deviceId}`);
  }
}

實務觀點:當資料來源是外部 API,屬性名稱往往是唯一辨識型別的關鍵,此時使用 in 可以寫出 更直觀且安全 的程式碼。


範例 5:在函式重載中搭配 in

function getValue(obj: { a: number } | { b: string }, key: "a"): number;
function getValue(obj: { a: number } | { b: string }, key: "b"): string;
function getValue(obj: any, key: "a" | "b"): any {
  if (key in obj) {
    return obj[key];
  }
  throw new Error(`Key ${key} not found`);
}

// 使用
const num = getValue({ a: 10 }, "a"); // num 為 number
const str = getValue({ b: "hello" }, "b"); // str 為 string

關鍵:在實作函式時,in 讓我們能安全地存取動態鍵,且不會破壞重載的型別推論。


常見陷阱與最佳實踐

陷阱 說明 解決方式
屬性名稱同時出現在多個型別 若聯合型別的成員都包含相同屬性,in 無法縮窄型別。 使用 kind discriminant 或結合 typeofinstanceof 進一步區分。
in 只檢查鍵是否存在,而非值是否為 undefined 可選屬性若值為 undefined,仍會回傳 true 在需要確保值存在時,額外檢查 obj[prop] !== undefined
對於 null / undefined 使用 in 會拋錯 nullundefined 不是物件,會產生 runtime error。 先做 obj != null 的檢查,或使用 可選鏈結 (obj?.prop).
在泛型中直接使用字面字串 若泛型參數不保證有該屬性,編譯器會報錯。 使用 型別謂詞 (obj is Record<K, unknown>) 或 keyof 限制泛型。
過度依賴 in,忽略 type guard 函式 直接寫大量 if ("x" in obj) 會降低可讀性。 把判斷抽成 可重用的型別保護函式(如 hasProp),提升維護性。

最佳實踐

  1. 優先使用 discriminated union(如 kindtype 字段)配合 switch,結構更清晰。
  2. 在需要動態屬性檢查時,才使用 in,並搭配 型別謂詞 讓編譯器自動縮窄。
  3. 避免在 null/undefined 上直接使用 in,先做 null 檢查或使用可選鏈結。
  4. 對於可選屬性,若只關心「是否被賦值」而非「是否存在鍵」時,使用 !== undefined
  5. 將常用的 in 判斷抽成工具函式,讓程式碼保持 DRY(Don't Repeat Yourself)。

實際應用場景

1. 前端表單資料驗證

在表單提交前,我們常需要根據欄位是否存在來決定驗證規則:

interface BaseForm { name: string; }
interface EmailForm extends BaseForm { email: string; }
interface PhoneForm extends BaseForm { phone: string; }

function validate(form: EmailForm | PhoneForm) {
  if ("email" in form) {
    // email 必填且格式正確
    if (!/^\S+@\S+\.\S+$/.test(form.email)) {
      throw new Error("Invalid email");
    }
  } else {
    // phone 必填且只能是數字
    if (!/^\d+$/.test(form.phone)) {
      throw new Error("Invalid phone number");
    }
  }
}

2. 後端 API 回傳的多樣化結果

某些 REST API 會根據狀態碼回傳不同結構,使用 in 能在 服務端前端 直接判斷:

type ApiResult =
  | { status: 200; payload: { userId: string } }
  | { status: 400; error: string }
  | { status: 401; reason: "unauthenticated" | "expiredToken" };

function processResult(r: ApiResult) {
  if ("payload" in r) {
    console.log(`User ID: ${r.payload.userId}`);
  } else if ("error" in r) {
    console.warn(`Bad request: ${r.error}`);
  } else {
    console.error(`Auth issue: ${r.reason}`);
  }
}

3. 動態插件系統

在大型應用中,插件可能會提供不同的 API。透過 in 可安全地呼叫插件提供的功能:

type Plugin =
  | { init(): void; version: string }
  | { render(container: HTMLElement): void; theme?: string };

function usePlugin(p: Plugin) {
  if ("init" in p) {
    p.init();
    console.log(`Plugin version ${p.version}`);
  } else {
    const container = document.getElementById("app")!;
    p.render(container);
    if (p.theme) {
      container.dataset.theme = p.theme;
    }
  }
}

4. 讀取設定檔或環境變數

設定檔中可能只包含部份欄位,使用 in 能在載入時自動補足預設值:

interface EnvConfig {
  apiUrl: string;
  timeout?: number;
  debug?: boolean;
}

function loadConfig(raw: Partial<EnvConfig>): EnvConfig {
  const defaults: Required<Pick<EnvConfig, "timeout" | "debug">> = {
    timeout: 5000,
    debug: false,
  };
  return {
    apiUrl: raw.apiUrl,
    timeout: "timeout" in raw ? raw.timeout! : defaults.timeout,
    debug: "debug" in raw ? raw.debug! : defaults.debug,
  };
}

總結

  • in 運算子不僅是 檢查屬性是否存在 的工具,更是 型別保護 的關鍵利器。
  • 透過 in,TypeScript 能在條件分支中 自動縮窄聯合型別,讓開發者安全地存取屬性與方法。
  • 結合 keyof型別謂詞(type predicate)與 泛型,可打造通用且可重用的型別保護函式。
  • 使用時要留意 可選屬性null/undefined 的情況,並避免在所有成員皆擁有相同屬性時過度依賴 in
  • 在實務上,in 常用於 表單驗證、API 回應處理、插件系統、設定檔載入 等需要根據屬性存在與否決策的場景。

掌握 in 運算子的型別保護技巧,能讓你的 TypeScript 程式碼在 安全性、可讀性與維護性 上都有顯著提升。祝你在日常開發中玩得開心、寫得更好! 🚀