TypeScript 型別守衛文章封面

TypeScript 的型別系統強大且靈活,但在處理聯合型別(Union Types)時,我們常需要在運行時確定具體的型別。型別守衛(Type Guards)就是解決這個問題的關鍵技術。

什麼是型別守衛?

型別守衛是一種能在特定作用域內縮小型別範圍的技術。透過型別守衛,TypeScript 編譯器能夠推斷出更精確的型別。

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function processValue(value: string | number) {
  if (isString(value)) {
    // 這裡 TypeScript 知道 value 是 string
    console.log(value.toUpperCase());
  } else {
    // 這裡 TypeScript 知道 value 是 number
    console.log(value.toFixed(2));
  }
}

內建型別守衛

typeof 守衛

最基本的型別守衛,適用於原始型別:

function format(value: string | number) {
  if (typeof value === 'string') {
    return value.trim();
  }
  return value.toFixed(2);
}

instanceof 守衛

用於判斷類別實例:

class ApiError extends Error {
  statusCode: number;
}

function handleError(error: Error | ApiError) {
  if (error instanceof ApiError) {
    console.log(`API Error: ${error.statusCode}`);
  } else {
    console.log('Generic Error');
  }
}

in 守衛

檢查物件是否包含特定屬性:

interface User {
  name: string;
  email: string;
}

interface Admin {
  name: string;
  permissions: string[];
}

function greet(person: User | Admin) {
  if ('permissions' in person) {
    console.log(`Admin: ${person.name}`);
  } else {
    console.log(`User: ${person.name}`);
  }
}

自定義型別守衛

使用 is 關鍵字創建自定義型別守衛:

interface Cat {
  meow: () => void;
}

interface Dog {
  bark: () => void;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return 'meow' in animal;
}

function makeSound(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow();
  } else {
    animal.bark();
  }
}

進階技巧

斷言函數(Assertion Functions)

TypeScript 3.7+ 支援斷言函數,可以直接拋出錯誤:

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Value must be a string');
  }
}

function process(value: unknown) {
  assertIsString(value);
  // 這裡 value 被縮小為 string 型別
  console.log(value.toUpperCase());
}

判別聯合(Discriminated Unions)

使用共同屬性區分不同型別:

interface Success {
  status: 'success';
  data: string;
}

interface Error {
  status: 'error';
  message: string;
}

type Response = Success | Error;

function handleResponse(response: Response) {
  if (response.status === 'success') {
    console.log(response.data);
  } else {
    console.error(response.message);
  }
}

常見陷阱

1. 忘記處理所有情況

// ❌ 不完整的型別守衛
function process(value: string | number | boolean) {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  // 忘記處理 boolean 的情況
  return value.toFixed(2); // 錯誤!
}

2. 過度使用 any

// ❌ 避免使用 any
function isUser(value: any): value is User {
  return value && value.name && value.email;
}

// ✅ 使用 unknown
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'name' in value &&
    'email' in value
  );
}

最佳實踐

  1. 優先使用內建型別守衛:簡單場景下,typeofinstanceof 就足夠
  2. 使用判別聯合:為不同型別添加 typekind 屬性
  3. 編寫可重用的型別守衛:將常用的型別檢查抽取為函數
  4. 避免過度複雜:型別守衛應該簡單明瞭

總結

型別守衛是 TypeScript 型別系統中不可或缺的一部分。掌握這些技巧能讓你的代碼更安全、更易維護。從簡單的 typeof 開始,逐步使用更進階的技術,讓 TypeScript 的型別系統為你的開發保駕護航。