Pick<T, K> 是一个内置工具类型,它创建的对象类型只包含 T 中 K 所列出的属性。当一个函数或组件只需要某个大类型中的几个字段时,就可以用它:
interface User {
id: number;
name: string;
email: string;
passwordHash: string;
lastLogin: string;
}
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }
function renderPreview(user: UserPreview): string {
return `#${user.id} ${user.name}`;
}
const ada: User = {
id: 1, name: "Ada", email: "ada@example.com",
passwordHash: "x9f...", lastLogin: "2024-03-15",
};
console.log(renderPreview(ada)); // #1 Ada
console.log(renderPreview({ id: 2, name: "Linus" })); // #2 Linus
renderPreview 既接受完整的 User,也接受普通的 { id, name } 对象,因为它只要求自己用到的字段。这样它更容易调用,也更容易测试。Pick 只作用于类型:运行时,完整的 ada 对象仍然拥有全部五个属性。
语法与键检查
Pick<Type, Keys>
Keys 可以是单个键,也可以是多个键组成的联合类型,并且每个键都必须在 Type 上存在。TypeScript 标准库中的定义把这一点写成了约束:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
由于有 K extends keyof T,拼错或不存在的键会导致编译错误,编辑器也能自动补全键名:
interface Product {
sku: string;
title: string;
price: number;
stock: number;
}
type PriceTag = Pick<Product, "title" | "price">;
const tag: PriceTag = { title: "Lamp", price: 39 };
console.log(`${tag.title}: $${tag.price}`); // Lamp: $39
// @ts-expect-error: Type '"cost"' does not satisfy the constraint 'keyof Product'
type Broken = Pick<Product, "cost">;
错误信息是 error TS2344: Type '"cost"' does not satisfy the constraint 'keyof Product'.。这一点和 Omit 不同,Omit 的键不会被检查。
Pick 保留 readonly 和可选修饰符
Pick 是对 T 的键做映射,所以每个被选取的属性都会保留自己的修饰符:
interface Account {
readonly id: number;
email?: string;
plan: "free" | "pro";
}
type AccountRef = Pick<Account, "id" | "email">;
// { readonly id: number; email?: string }
const ref: AccountRef = { id: 7 }; // email is still optional
// @ts-expect-error: Cannot assign to 'id' because it is a read-only property
ref.id = 8;
console.log(ref); // { id: 8 }
最后一行输出 { id: 8 },因为 readonly 只是编译时检查:这次赋值被报告了,又被 @ts-expect-error 压制,最后照样执行了。如果想在选取的同时改变修饰符,就把结果包一层:Partial<Pick<Account, "plan">> 或 Required<Pick<Account, "email">>。
Pick 与 Omit 对比
Pick 指定要保留什么,Omit 指定要去掉什么。今天两者可能给出相同的结果,但当源类型增加属性时,它们的表现就不一样了:
interface Order {
id: number;
total: number;
cardLast4: string;
// added later: internalNotes: string;
}
type Receipt = Pick<Order, "id" | "total">; // stays { id; total }
type AdminView = Omit<Order, "cardLast4">; // will include internalNotes too
const receipt: Receipt = { id: 1, total: 49.9 };
const admin: AdminView = { id: 1, total: 49.9 };
console.log(receipt, admin); // { id: 1, total: 49.9 } { id: 1, total: 49.9 }
| Pick<T, K> | Omit<T, K> |
|---|
| 你列出的是 | 要保留的键 | 要移除的键 |
T 上的新属性 | 不会包含 | 自动包含 |
K 中有未知的键 | 编译错误(TS2344) | 静默接受 |
| 适用于 | API 响应、公开视图、props | “除了机密字段之外的全部” |
对于要离开服务器的数据,Pick 是更安全的默认选择:除非有人有意添加,否则什么都不会出现在类型里。
从嵌套类型中选取
Pick 只能看到顶层的键:像 Pick<Customer, "address.city"> 这样的点号路径会报错(TS2344),而不是选取嵌套属性。先用索引访问类型取到嵌套类型,再从中选取:
interface Customer {
id: number;
name: string;
address: {
street: string;
city: string;
country: string;
};
}
type City = Pick<Customer["address"], "city">; // { city: string }
// Keep the shape, trim the nested object
type ShippingLabel = Pick<Customer, "name"> & {
address: Pick<Customer["address"], "city" | "country">;
};
const label: ShippingLabel = {
name: "Ada",
address: { city: "London", country: "UK" },
};
const city: City = { city: label.address.city };
console.log(label, city.city);
// { name: 'Ada', address: { city: 'London', country: 'UK' } } London
Customer["address"] 就是 address 属性的类型。同样的语法也可以取到数组元素:对于 orders: Order[] 属性,Customer["orders"][number] 就是 Order。
带类型的 pick() 函数
Pick 在运行时什么也不做。一个小型泛型函数可以把选中的属性复制到新对象中,并且返回值的类型恰好是 Pick<T, K>:
function pick<T extends object, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = obj[key];
}
return result;
}
const book = { id: 3, title: "Dune", author: "Herbert", pages: 412 };
const summary = pick(book, "title", "author"); // { title: string; author: string }
console.log(summary); // { title: 'Dune', author: 'Herbert' }
console.log(Object.keys(summary)); // [ 'title', 'author' ]
// pick(book, "isbn") would be error TS2345: Argument of type '"isbn"' is not assignable
// to parameter of type '"author" | "id" | "pages" | "title"'.
K extends keyof T 让 TypeScript 能从字符串参数推断出 K,所以返回类型恰好只包含被选取的键。之所以需要 {} as Pick<T, K> 断言,是因为空对象一开始并没有这些属性;循环会把它们填进去。
按值类型选取属性
Pick 按键名选取。如果要按属性的类型选取,比如“所有字符串字段”,就写一个带键过滤的映射类型:
type PickByValue<T, V> = {
[P in keyof T as T[P] extends V ? P : never]: T[P];
};
interface Article {
id: number;
title: string;
slug: string;
views: number;
published: boolean;
}
type TextFields = PickByValue<Article, string>; // { title: string; slug: string }
type NumberFields = PickByValue<Article, number>; // { id: number; views: number }
const text: TextFields = { title: "Hello", slug: "hello" };
const nums: NumberFields = { id: 1, views: 120 };
console.log(text, nums); // { title: 'Hello', slug: 'hello' } { id: 1, views: 120 }
as 子句在属性类型匹配时把键映射为它自己,否则映射为 never,而 never 键会被丢弃。有一个细节:可选属性的类型包含 undefined,所以这里可选的 subtitle?: string 不会被算作 string;要把它包含进来,就用 string | undefined 来判断。