本文 AI 產出,尚未審核

TypeScript 工具型別:Pick<T, K>

簡介

在大型前端或 Node.js 專案中,型別的可維護性往往直接影響開發效率與程式的可靠度。
TypeScript 提供了一系列的 Utility Types,讓我們可以在不重複撰寫介面(interface)或型別別名的前提下,快速產生新的型別。其中,Pick<T, K> 是最常使用且最實用的工具之一,它能從既有型別 T 中挑選出指定的屬性 K,組成全新的型別。

為什麼會需要「挑選」屬性?

  • API 回傳資料 常只需要部份欄位,直接使用 Pick 可以避免手動寫冗長的介面。
  • 表單或 UI 元件 只關心模型的部分屬性,使用 Pick 可讓型別與實際需求保持同步。
  • 程式碼重構 時,若想把大型介面切割成更小的片段,Pick 提供了安全且可讀性高的方式。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹 Pick<T, K>,幫助你在實務開發中快速上手並避免踩雷。


核心概念

什麼是 Pick<T, K>

Pick<T, K> 是一個 泛型型別,接受兩個參數:

參數 說明
T 來源型別(可以是介面、型別別名或類別)
K 欲挑選的屬性鍵集合,必須是 T 中已存在的屬性名稱(使用字面量聯合型別)

語法:

type MyPicked = Pick<T, K>;

Pick 會返回一個新型別,僅保留 TK 所指定的屬性,且屬性 保持原本的可選性與只讀性

小技巧K 必須使用 keyof T 或其子集合,否則編譯會報錯,保證了型別安全。


為什麼 Pick 會保留屬性的修飾符?

在 TypeScript 中,介面的屬性可以被標記為 readonly?(可選)。Pick 內部的實作是透過映射型別(Mapped Types)完成的:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

簡單來說,它遍歷 K 中的每個鍵 P,直接取出 T 中對應的屬性型別 T[P],因此原本的 readonly? 都會被完整保留下來。


程式碼範例

以下示範 5 個常見且實用的 Pick 用法,從基礎到進階,一步步帶你熟悉這個工具型別。

1️⃣ 基本挑選

interface User {
  id: number;
  name: string;
  email: string;
  isAdmin: boolean;
}

// 只需要 id 與 name
type UserPreview = Pick<User, "id" | "name">;

const preview: UserPreview = {
  id: 1,
  name: "Alice",
  // email: "alice@example.com", // ❌ 多餘屬性會編譯錯誤
};

說明UserPreview 只保留 idname,其餘屬性被完整剔除,對於只顯示簡略資訊的 UI 非常適合。


2️⃣ 搭配 keyof 動態挑選

type UserKeys = keyof User; // "id" | "name" | "email" | "isAdmin"

type PartialUser = Pick<User, UserKeys>; // 等同於原本的 User

// 若只想挑選所有以 "is" 開頭的屬性,需要先過濾鍵名
type BooleanKeys = {
  [K in keyof User]: User[K] extends boolean ? K : never
}[keyof User]; // "isAdmin"

type AdminFlag = Pick<User, BooleanKeys>;

const admin: AdminFlag = { isAdmin: true };

說明:透過條件型別與映射型別,我們可以動態算出需要挑選的鍵,讓 Pick 更具彈性。


3️⃣ 產生「只讀」版本的子集合

interface Config {
  url: string;
  timeout: number;
  retry?: number;
}

// 只需要 url,且不允許被改寫
type ReadonlyUrl = Readonly<Pick<Config, "url">>;

const cfg: ReadonlyUrl = { url: "https://api.example.com" };
// cfg.url = "https://other.com"; // ❌ 編譯錯誤,屬性為 readonly

說明Pick 本身不會改變屬性修飾符,但我們可以與 ReadonlyPartial 等其他 Utility Types 組合,打造符合需求的型別。


4️⃣ 用於函式參數的「精簡」型別

interface CreatePostPayload {
  title: string;
  content: string;
  authorId: number;
  tags?: string[];
  isPublic: boolean;
}

// 後端只接受 title、content、tags
type PostInput = Pick<CreatePostPayload, "title" | "content" | "tags">;

function createPost(data: PostInput) {
  // 這裡的 data 只會有三個屬性,其他屬性不可能傳入
  console.log(data);
}

createPost({
  title: "Hello",
  content: "World",
  tags: ["typescript", "utility"]
});

說明:在 API 客戶端或服務端,使用 Pick 讓函式簽名只接受必要的欄位,減少錯誤傳遞不相關資訊的風險。


5️⃣ 與 泛型 結合:打造可重用的「資料挑選」函式

function selectProps<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach((key) => {
    result[key] = obj[key];
  });
  return result;
}

interface Product {
  id: number;
  name: string;
  price: number;
  description?: string;
}

const p: Product = {
  id: 101,
  name: "Keyboard",
  price: 2999,
  description: "機械式鍵盤"
};

const short = selectProps(p, "id", "name");
 // short 的型別為 { id: number; name: string; }

說明:透過泛型 K extends keyof TselectProps 能在執行時挑選任意屬性,同時在編譯期保證回傳型別正確,實作上相當安全且易於維護。


常見陷阱與最佳實踐

陷阱 說明 解決方式
K 不是 keyof T 若傳入不存在於 T 的鍵,編譯會錯誤。 使用 K extends keyof T 限制泛型,或先用 Extract<...> 濾除不合法鍵。
屬性遺失可選性 有時開發者誤以為 Pick 會把 ? 變成必填。實際上會保留原本的可選性。 確認介面中 ? 的行為,若需要改變可選性,配合 Required<T>Partial<T> 使用。
深層結構不會自動遞迴 Pick 只會挑選第一層屬性,若屬性本身是物件,裡面的子屬性不會被篩選。 需要自行結合 遞迴映射型別(如 DeepPick)或在子層級再次使用 Pick
與交叉型別 (&) 結合時的屬性衝突 Pick<A, K> & Pick<B, K> 可能會產生相同鍵的不同型別,導致交叉錯誤。 盡量避免在同一屬性上同時使用多個 Pick,或使用 Omit 先排除衝突鍵。
過度使用 Pick 造成型別膨脹 在大量組合型別時,過度 Pick 會讓型別圖譜變得複雜,編譯速度下降。 只在必要時使用 Pick,對於常見的子集合可考慮直接定義獨立介面。

最佳實踐

  1. 明確命名:對於挑選出來的子型別,使用語意清晰的名稱(如 UserPreviewPostInput),提升可讀性。
  2. 結合 Partial/Required:根據需求調整屬性的必填/可選性,而不是在介面本身修改。
  3. 使用 as const:在傳遞鍵名陣列給泛型函式時,使用 as const 讓編譯器推斷出字面量聯合型別,避免寬鬆的 string[]
  4. 保持單一職責Pick 只負責「挑選」屬性,不應同時做「改變屬性型別」的工作,若需要轉型,另寫轉換函式。

實際應用場景

1️⃣ 前端表單的「編輯」與「檢視」模式

在 CRUD 系統中,編輯表單 只需要可寫入的欄位,而 檢視頁面 只需要顯示欄位。利用 Pick 可以分別建立兩套型別:

interface Article {
  id: number;
  title: string;
  content: string;
  authorId: number;
  createdAt: Date;
  updatedAt: Date;
}

// 編輯表單只允許修改 title 與 content
type ArticleEditForm = Pick<Article, "title" | "content">;

// 檢視頁面顯示全部資訊(只讀)
type ArticleView = Omit<Article, "authorId">; // 例子:排除不必要欄位

2️⃣ API 客戶端的「請求參數」與「回傳結果」分離

後端 API 常會回傳比請求參數更多的欄位。使用 Pick 可以為 請求體 建立最小化的型別,避免把不必要的資料送給伺服器。

interface SearchResponse {
  total: number;
  items: Array<{
    id: string;
    name: string;
    price: number;
    stock: number;
    description?: string;
  }>;
  page: number;
  pageSize: number;
}

// 客戶端只需要 id, name, price 作為列表顯示
type SearchResultItem = Pick<SearchResponse["items"][0], "id" | "name" | "price">;

3️⃣ 多語系(i18n)資源的子集合

假設有一份完整的語系檔案,裡面包含大量鍵值。若某個子模組只需要其中一小部分,可利用 Pick 產生 子語系型別,讓編譯器幫忙檢查是否遺漏。

interface Locale {
  welcome: string;
  logout: string;
  profile: string;
  settings: string;
  // ...上百個鍵
}

// Settings 模組只需要以下兩個字串
type SettingsLocale = Pick<Locale, "settings" | "logout">;

4️⃣ 共享型別的「最小化」版本(如 Redux State)

在 Redux 中,我們經常需要把 全局狀態 中的某些片段傳遞給子 component。Pick 能快速產出只包含必要欄位的型別,避免子 component 直接依賴過大的 state。

interface RootState {
  user: {
    id: string;
    name: string;
    email: string;
    token: string;
  };
  theme: {
    darkMode: boolean;
  };
  // 其他模組…
}

// Header 只需要 user.id 與 user.name
type HeaderUserInfo = Pick<RootState["user"], "id" | "name">;

總結

  • Pick<T, K> 是 TypeScript 提供的 映射型別工具,能從來源型別 T 中挑選出一組屬性 K,生成新型別。
  • 它保留原屬性的 readonly? 修飾符,且在編譯期即保證鍵的合法性。
  • 透過結合 keyof、條件型別、PartialReadonly 等其他 Utility Types,我們可以靈活構建 只讀、可選、深層挑選 等多種變形。
  • 常見的陷阱包括鍵名不屬於來源型別、屬性可選性誤解以及深層結構不會自動遞迴等,遵循最佳實踐(明確命名、適度使用、結合 as const)即可減少錯誤。
  • 在實務開發中,Pick 常見於 API 請求/回應、表單資料、國際化資源、Redux state 切片 等情境,能顯著提升程式碼的可讀性與維護性。

掌握 Pick<T, K> 後,你就能在日常開發中 快速抽取型別子集、減少重複定義、提升型別安全,讓 TypeScript 的型別系統真正成為你開發的利器。祝你寫程式愉快! 🚀