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 會返回一個新型別,僅保留 T 中 K 所指定的屬性,且屬性 保持原本的可選性與只讀性。
小技巧:
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只保留id、name,其餘屬性被完整剔除,對於只顯示簡略資訊的 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本身不會改變屬性修飾符,但我們可以與Readonly、Partial等其他 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 T,selectProps能在執行時挑選任意屬性,同時在編譯期保證回傳型別正確,實作上相當安全且易於維護。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
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,對於常見的子集合可考慮直接定義獨立介面。 |
最佳實踐:
- 明確命名:對於挑選出來的子型別,使用語意清晰的名稱(如
UserPreview、PostInput),提升可讀性。 - 結合
Partial/Required:根據需求調整屬性的必填/可選性,而不是在介面本身修改。 - 使用
as const:在傳遞鍵名陣列給泛型函式時,使用as const讓編譯器推斷出字面量聯合型別,避免寬鬆的string[]。 - 保持單一職責:
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、條件型別、Partial、Readonly等其他 Utility Types,我們可以靈活構建 只讀、可選、深層挑選 等多種變形。 - 常見的陷阱包括鍵名不屬於來源型別、屬性可選性誤解以及深層結構不會自動遞迴等,遵循最佳實踐(明確命名、適度使用、結合
as const)即可減少錯誤。 - 在實務開發中,
Pick常見於 API 請求/回應、表單資料、國際化資源、Redux state 切片 等情境,能顯著提升程式碼的可讀性與維護性。
掌握 Pick<T, K> 後,你就能在日常開發中 快速抽取型別子集、減少重複定義、提升型別安全,讓 TypeScript 的型別系統真正成為你開發的利器。祝你寫程式愉快! 🚀