iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
Modern Web

我與型別的 30 天約定:TypeScript 入坑實錄系列 第 22

Day 22|型別守衛與類型窄化:讓 TypeScript 幫你聰明收斂型別

  • 分享至 

  • xImage
  •  

1) 為什麼需要型別守衛?

TS 的型別推論雖然很聰明,但在以下情境會「卡住」:

ts
CopyEdit
type ApiResult = User | ApiError;

function handle(result: ApiResult) {
  // result 可能是 User,也可能是 ApiError
  console.log(result.name); // ❌ TS 報錯:屬性 name 不存在於 ApiError
}

我們需要讓 TS 明白:「這個分支一定是某種型別」,

這就是型別守衛(Type Guard)的用途。


2) typeof 型別守衛

ts
CopyEdit
function printId(id: string | number) {
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // 這裡 TS 知道是 string
  } else {
    console.log(id.toFixed(2));    // 這裡 TS 知道是 number
  }
}

適用於string | number | boolean | bigint | symbol | undefined | object 這些基本型別判斷。


3) instanceof 型別守衛

ts
CopyEdit
class AppError extends Error {
  constructor(public code: string) {
    super(code);
  }
}

function handleError(err: unknown) {
  if (err instanceof AppError) {
    console.log(err.code); // 這裡 TS 知道 err 是 AppError
  }
}

適用於:檢查物件是否來自某個 class。


4) in 運算子型別守衛

ts
CopyEdit
type User = { id: string; name: string };
type ApiError = { code: string; message: string };

function handle(result: User | ApiError) {
  if ("name" in result) {
    console.log(result.name); // TS 知道是 User
  } else {
    console.log(result.code); // TS 知道是 ApiError
  }
}

適用於:物件聯合型別,透過屬性判斷分支。


5) 自訂型別守衛(User-Defined Type Guard)

x is SomeType 告訴 TS 在分支內的型別:

ts
CopyEdit
function isUser(obj: any): obj is User {
  return obj && typeof obj.id === "string" && typeof obj.name === "string";
}

function handle(result: User | ApiError) {
  if (isUser(result)) {
    console.log(result.name); // User
  } else {
    console.log(result.code); // ApiError
  }
}

好處

  • 可重複使用判斷邏輯
  • 在整個專案都能用來窄化型別

6) 可辨識聯合(Discriminated Union)

這是 TS 型別窄化的黃金模式,透過 共同屬性 + 字面量型別 讓 TS 自動分支。

ts
CopyEdit
type Success = { status: "success"; data: User };
type Fail = { status: "error"; error: ApiError };
type ApiResponse = Success | Fail;

function processResponse(res: ApiResponse) {
  if (res.status === "success") {
    console.log(res.data.name); // TS 自動知道是 Success
  } else {
    console.log(res.error.message); // TS 自動知道是 Fail
  }
}

好處

  • 無需額外 function 判斷
  • TS 自動根據共同屬性窄化型別

7) 實戰:結合 Day 20 + Day 21

假設我們有 Result<T, E> 型別:

ts
CopyEdit
type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function isOk<T, E>(res: Result<T, E>): res is { ok: true; value: T } {
  return res.ok;
}

function handleResult(res: Result<User, ApiError>) {
  if (isOk(res)) {
    console.log(res.value.name); // User
  } else {
    console.error(res.error.code); // ApiError
  }
}

這樣就不用在 if 裡面重複寫判斷條件。


8) 進階:配合 zod 的型別守衛

ts
CopyEdit
import { z } from "zod";

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
});

type User = z.infer<typeof userSchema>;

function isUser(data: unknown): data is User {
  return userSchema.safeParse(data).success;
}

好處:型別判斷與資料驗證合一。


9) 常見坑

  1. 型別守衛只影響 TS 編譯期
    • 執行期還是要寫真實的防呆(特別是 API 回應)。
  2. 自訂守衛要回傳 obj is Type
    • 不要回傳 boolean,那樣 TS 不會自動窄化。
  3. 不要過度守衛
    • 有些情況可用 Discriminated Union 讓 TS 自動判斷,程式會更簡潔。

上一篇
Day 21|TypeScript 工具型別實戰:Pick / Omit / Partial / Required / Record / ReturnType 全解
下一篇
Day 23|泛型進階:打造可重用且型別安全的工具函式
系列文
我與型別的 30 天約定:TypeScript 入坑實錄24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言