本文 AI 產出,尚未審核

TypeScript 教學 – 工具型別(Utility Types)

主題:Extract<T, U>


簡介

在大型前端或 Node.js 專案中,型別安全是維持程式碼品質的關鍵。TypeScript 內建的工具型別(Utility Types)讓開發者能以極少的程式碼,對既有型別進行重新組合、過濾或轉換,從而減少重複定義與錯誤。
其中,Extract<T, U> 是一個 條件型別(Conditional Type),專門用來 從型別 T 中挑選出同時屬於 U 的成員。簡單來說,它會把 T 內「符合」U 條件的部分「抽取」出來。

為什麼會需要這樣的功能?

  • API 回傳型別 常常是聯合型別(Union),但我們只需要其中一部份。
  • 事件系統 會有多種事件類型,開發者只想處理特定事件。
  • 資料驗證 時,想從一組可能的錯誤代碼中挑出特定範圍。

掌握 Extract 能讓你寫出更 精準可讀、且 易於維護 的型別宣告。


核心概念

1. Extract<T, U> 的定義

type Extract<T, U> = T extends U ? T : never;
  • T extends U ? T : never 表示:如果 T 可以指派給 U,就保留 T;否則回傳 never
  • T聯合型別 時,條件會對每個成員分別求值,最後組合成新的聯合型別。
  • never 代表「不存在的型別」,會在結果中自動被過濾掉。

2. 基本範例

type AllColors = "red" | "green" | "blue" | "yellow";
type PrimaryColors = Extract<AllColors, "red" | "blue">;
//  => "red" | "blue"

這裡 AllColors 包含四種顏色,但我們只想保留「紅」與「藍」兩種,Extract 正好完成這件事。

3. 與 Exclude 的關係

  • Exclude<T, U>:從 T剔除 符合 U 的成員。
  • Extract<T, U>:從 T抽取 符合 U 的成員。

兩者其實是互補的:

type NotPrimary = Exclude<AllColors, PrimaryColors>; // "green" | "yellow"
type OnlyPrimary = Extract<AllColors, PrimaryColors>; // "red" | "blue"

4. 進階範例 1:抽取 API 回傳的成功狀態

假設有一個 API 回傳的型別:

type ApiResponse =
  | { status: 200; data: string }
  | { status: 201; data: string }
  | { status: 400; error: string }
  | { status: 404; error: string };

我們只想取得 成功(2xx)的回傳型別:

type SuccessResponse = Extract<ApiResponse, { status: 200 } | { status: 201 }>;
/*
  => {
    status: 200; data: string;
  } | {
    status: 201; data: string;
  }
*/

使用 Extract,不需要手動寫兩個介面,型別會自動對齊原本的結構。

5. 進階範例 2:事件系統的類型過濾

type EventMap = {
  click: { x: number; y: number };
  keypress: { key: string };
  resize: { width: number; height: number };
};

type AllEvents = keyof EventMap; // "click" | "keypress" | "resize"
type MouseEvents = Extract<AllEvents, "click" | "resize">;
// => "click" | "resize"

在事件處理函式中,我們可以限制只能接受 MouseEvents

function handleMouseEvent<E extends MouseEvents>(event: E, payload: EventMap[E]) {
  // 只會被呼叫 click 或 resize 事件
}

6. 進階範例 3:從錯誤代碼中抽取特定類別

type ErrorCode = 
  | "E001" // 網路錯誤
  | "E002" // 驗證失敗
  | "E003" // 權限不足
  | "E004" // 超時
  | "E005"; // 未知錯誤

// 想要抽出「驗證」相關的錯誤
type ValidationErrors = Extract<ErrorCode, "E002" | "E005">;
// => "E002" | "E005"

此時若要建立對應的錯誤訊息映射表,只需要針對 ValidationErrors 產生:

const validationMsg: Record<ValidationErrors, string> = {
  E002: "資料驗證失敗",
  E005: "未知錯誤,請稍後再試",
};

常見陷阱與最佳實踐

陷阱 說明 解法 / 建議
U 不是聯合型別 U 是單一字面值或介面,Extract 仍會正常運作,但可能無法達到「過濾」的預期。 確認 U聯合型別,或使用 Extract<T, any> 只想保留 T 中可指派的部分。
never 結果未被注意 TU 完全不匹配,結果會是 never,使用時會產生「不可指派」的錯誤。 在使用前先檢查 Extract<T, U> 是否為 never,或加入 extends never ? never : ... 的防護。
過度依賴 any 有時開發者會把 U 設為 any,這會把 T 完全保留,失去過濾意義。 僅在真的需要「全部」時才使用 any,平時保持 U 為具體的型別集合。
與映射型別(Mapped Types)混用時的可讀性 Extract 直接寫在長長的映射型別裡,會讓型別變得難以追蹤。 建議先 抽離 成獨立的型別別名,再在映射型別中引用,提升可讀性。

最佳實踐

  1. 先定義「篩選條件」的型別,再使用 Extract
    type SuccessStatus = 200 | 201;
    type SuccessResponse = Extract<ApiResponse, { status: SuccessStatus }>;
    
  2. 結合 RecordPartial 等工具型別,打造自動化的映射表。
    type MsgMap<T extends string> = Partial<Record<T, string>>;
    type ValidationMsg = MsgMap<ValidationErrors>;
    
  3. 在函式泛型中使用 Extract,保證傳入的參數符合特定子集合。
    function send<T extends EventMap[keyof EventMap]>(payload: Extract<T, { type: "click" }>) { ... }
    

實際應用場景

1. 前端表單驗證

在大型表單中,錯誤代碼往往是 聯合型別。使用 Extract 可以快速取得「必填欄位」的錯誤代碼,並只顯示相關訊息。

type FormError = 
  | "REQUIRED" 
  | "INVALID_EMAIL" 
  | "TOO_SHORT" 
  | "TOO_LONG";

type RequiredErrors = Extract<FormError, "REQUIRED" | "TOO_SHORT">;

const errorMsg: Record<RequiredErrors, string> = {
  REQUIRED: "此欄位為必填",
  TOO_SHORT: "字數過少",
};

2. Redux / NgRx Action 過濾

在 Redux 的 action 集合中,只想針對特定模組的 action 進行 reducer 處理:

type AppAction =
  | { type: "user/login"; payload: { name: string } }
  | { type: "user/logout" }
  | { type: "product/add"; payload: { id: number } }
  | { type: "product/remove"; payload: { id: number } };

type UserAction = Extract<AppAction, { type: "user/login" } | { type: "user/logout" }>;

function userReducer(state: UserState, action: UserAction) {
  // 只會收到 login / logout 兩種 action
}

3. GraphQL / REST API 客戶端

當 API 回傳的型別是 多種結果(成功、失敗、驗證錯誤),Extract 能幫助客戶端只取出需要的結果,避免在程式碼中處處檢查 status

type GraphQLResult =
  | { __typename: "Success"; data: User }
  | { __typename: "AuthError"; message: string }
  | { __typename: "NetworkError"; code: number };

type SuccessResult = Extract<GraphQLResult, { __typename: "Success" }>;
// => { __typename: "Success"; data: User }

總結

Extract<T, U> 是 TypeScript 中極為實用的條件工具型別,能夠從聯合型別 T 中「抽取」出符合 U 條件的子集合。透過它,我們可以:

  • 精準篩選 API、事件、錯誤代碼等多樣型別。
  • 減少手動重複 的型別宣告,提升程式碼可維護性。
  • 結合其他工具型別(如 RecordPartial)打造自動化、類型安全的映射表。

在實務開發中,適時使用 Extract 能讓型別系統更貼合業務需求,降低錯誤率,同時保持程式碼的可讀性與彈性。建議在面對 聯合型別條件過濾特定子集合 時,優先考慮 Extract,並遵守前述的最佳實踐,以免落入常見陷阱。

祝你在 TypeScript 的型別世界裡玩得開心,寫出更安全、更優雅的程式碼! 🚀