MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript Pick 工具类型的灵活运用

2024-03-113.1k 阅读

一、TypeScript 工具类型概述

在 TypeScript 的类型系统中,工具类型(Utility Types)是一组非常实用的类型操作符,它们能够帮助开发者更高效地操作和转换类型。这些工具类型通过对已有类型进行加工,生成新的类型,从而简化类型定义的过程,提高代码的可维护性和复用性。例如,PartialRequiredReadonly 等工具类型,它们分别将对象类型的属性变为可选、必选和只读。而 Pick 工具类型在这其中有着独特的用途,专注于从现有类型中挑选出特定的属性集合来创建新类型。

二、Pick 工具类型的基本定义与语法

Pick 工具类型用于从一个类型中挑选出指定的属性,从而创建一个新的类型。它的定义如下:

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

这里,T 是源类型,K 是一个联合类型,且 K 的每个成员都必须是 T 类型的键。Pick 工具类型会创建一个新类型,这个新类型只包含 T 中由 K 指定的属性,并且这些属性的类型与 T 中对应属性的类型保持一致。

下面来看一个简单的代码示例:

// 定义一个源类型
type User = {
    name: string;
    age: number;
    email: string;
};

// 使用 Pick 挑选属性
type UserInfo = Pick<User, 'name' | 'email'>;

// 创建 UserInfo 类型的实例
const userInfo: UserInfo = {
    name: 'John Doe',
    email: 'johndoe@example.com'
};

在上述代码中,我们定义了 User 类型,它有 nameageemail 三个属性。然后使用 Pick 工具类型从 User 类型中挑选出 nameemail 属性,创建了新类型 UserInfo。最后,我们创建了 UserInfo 类型的实例 userInfo,它只包含 nameemail 属性。

三、Pick 在实际项目中的基础应用场景

  1. 数据传输对象(DTO)的构建 在前后端交互中,前端经常需要向服务器发送特定的数据结构,而这个数据结构可能只是完整对象的一部分属性。例如,在用户注册场景中,前端可能只需要发送用户名和密码,而不需要发送用户的详细个人资料。
// 假设后端期望的用户注册数据结构
type UserRegisterDTO = Pick<User, 'name' | 'password'>;

// 模拟注册函数
function registerUser(userData: UserRegisterDTO) {
    // 这里可以进行实际的注册逻辑,如发送 HTTP 请求
    console.log('Registering user:', userData);
}

// 模拟用户输入
const registerInput: UserRegisterDTO = {
    name: 'Jane Smith',
    password: 'password123'
};

registerUser(registerInput);
  1. 组件属性的提取 在 React 等前端框架中,组件通常只需要接收父组件传递的部分属性。例如,有一个 UserCard 组件,它只需要显示用户的姓名和头像,而不需要其他详细信息。
import React from'react';

type User = {
    name: string;
    age: number;
    avatar: string;
    bio: string;
};

// 提取 User 类型中 UserCard 组件需要的属性
type UserCardProps = Pick<User, 'name' | 'avatar'>;

const UserCard: React.FC<UserCardProps> = ({ name, avatar }) => (
    <div>
        <img src={avatar} alt={name} />
        <p>{name}</p>
    </div>
);

const user: User = {
    name: 'Bob Johnson',
    age: 30,
    avatar: 'https://example.com/avatar.jpg',
    bio: 'A software engineer'
};

// 传递 UserCardProps 类型的属性
<UserCard name={user.name} avatar={user.avatar} />

四、深入理解 Pick 的类型推导机制

  1. 属性类型的继承 Pick 创建的新类型会完全继承源类型中挑选出的属性的类型。这意味着,如果源类型的属性是复杂类型,如函数类型、对象类型等,Pick 创建的新类型中的对应属性也会保持相同的复杂类型。
type ComplexUser = {
    name: string;
    contact: {
        phone: string;
        email: string;
    };
    greet: () => string;
};

type EssentialUser = Pick<ComplexUser, 'name' | 'greet'>;

const essentialUser: EssentialUser = {
    name: 'Alice',
    greet: () => 'Hello!'
};

在这个例子中,ComplexUser 类型包含了一个对象类型属性 contact 和一个函数类型属性 greetPick 创建的 EssentialUser 类型继承了 namegreet 属性的类型,greet 仍然是一个函数类型。 2. 联合类型与键的匹配 Pick 的第二个参数 K 是一个联合类型,它必须是源类型 T 的键的子集。TypeScript 会严格检查 K 中的每个成员是否是 T 的键。如果 K 中有不属于 T 的键,将会报错。

type Product = {
    id: number;
    name: string;
    price: number;
};

// 正确:键都存在于 Product 类型中
type ProductSummary = Pick<Product, 'id' | 'name'>;

// 错误:不存在 'description' 键
// type InvalidProductSummary = Pick<Product, 'id' | 'description'>;

五、Pick 与其他工具类型的组合使用

  1. Pick 与 Partial 的组合 有时候,我们不仅要从一个类型中挑选部分属性,还希望这些属性是可选的。这时可以将 PickPartial 组合使用。
type FullSettings = {
    theme: string;
    fontSize: number;
    language: string;
};

// 挑选部分属性并使其可选
type OptionalSettings = Partial<Pick<FullSettings, 'theme' | 'language'>>;

const userSettings: OptionalSettings = {
    theme: 'dark'
    // fontSize 未提供,language 未提供
};
  1. Pick 与 Readonly 的组合 如果我们希望从一个类型中挑选出的属性是只读的,可以将 PickReadonly 组合。
type Document = {
    title: string;
    content: string;
    author: string;
};

// 挑选属性并使其只读
type ReadonlyDocumentInfo = Readonly<Pick<Document, 'title' | 'author'>>;

const docInfo: ReadonlyDocumentInfo = {
    title: 'TypeScript Tips',
    author: 'John'
    // 尝试修改会报错
    // docInfo.title = 'New Title';
};

六、在复杂类型结构中使用 Pick

  1. 嵌套对象中的 Pick 应用 当源类型是嵌套对象时,Pick 同样可以发挥作用。不过,需要注意的是,Pick 本身只作用于直接属性,如果要对嵌套对象中的属性进行挑选,可能需要结合其他操作。
type Company = {
    name: string;
    address: {
        street: string;
        city: string;
        country: string;
    };
    employees: {
        name: string;
        position: string;
    }[];
};

// 挑选公司名称和地址中的城市
type CompanyLocation = Pick<Company, 'name' | 'address'> & {
    address: Pick<Company['address'], 'city'>;
};

const companyLocation: CompanyLocation = {
    name: 'ABC Inc.',
    address: {
        city: 'New York'
    }
};

在这个例子中,我们首先使用 Pick 挑选出 Company 类型的 nameaddress 属性。然后,针对 address 属性,我们再次使用 Pick 挑选出 city 属性,并通过交叉类型 & 将这两个挑选结果合并。 2. 数组与对象混合类型中的 Pick 在实际项目中,可能会遇到数组与对象混合的复杂类型。例如,一个包含多个用户信息的数组,每个用户信息又是一个对象。

type User = {
    id: number;
    name: string;
    age: number;
};

type UserList = User[];

// 从每个用户对象中挑选出 id 和 name 属性
type UserIdNameList = {
    [Index in keyof UserList]: Pick<UserList[Index], 'id' | 'name'>;
};

const users: UserList = [
    { id: 1, name: 'Tom', age: 25 },
    { id: 2, name: 'Jerry', age: 30 }
];

const idNameUsers: UserIdNameList = users.map(user => ({
    id: user.id,
    name: user.name
})) as UserIdNameList;

在上述代码中,我们定义了 UserList 类型,它是一个 User 对象数组。然后通过索引类型 [Index in keyof UserList] 遍历数组的每个元素,并对每个元素使用 Pick 挑选出 idname 属性,从而得到 UserIdNameList 类型。

七、在函数参数和返回值类型中的应用

  1. 函数参数类型的筛选 在定义函数时,有时候我们希望函数接收的参数是某个复杂对象类型的部分属性。这时可以使用 Pick 来定义函数参数的类型。
type Employee = {
    id: number;
    name: string;
    department: string;
    salary: number;
};

// 定义一个只接收员工 id 和 name 的函数
function displayEmployeeBasicInfo(employee: Pick<Employee, 'id' | 'name'>) {
    console.log(`Employee ID: ${employee.id}, Name: ${employee.name}`);
}

const employee: Employee = {
    id: 101,
    name: 'Eve',
    department: 'Engineering',
    salary: 5000
};

displayEmployeeBasicInfo({ id: employee.id, name: employee.name });
  1. 函数返回值类型的定制 函数的返回值类型也可以使用 Pick 进行定制,以返回特定属性组成的对象。
type Product = {
    id: number;
    name: string;
    price: number;
    description: string;
};

// 定义一个函数,返回产品的 id 和 name
function getProductSummary(product: Product): Pick<Product, 'id' | 'name'> {
    return { id: product.id, name: product.name };
}

const product: Product = {
    id: 1,
    name: 'Widget',
    price: 10.99,
    description: 'A useful widget'
};

const summary = getProductSummary(product);
console.log(summary);

八、使用 Pick 优化代码的可维护性与扩展性

  1. 代码重构中的 Pick 在项目的演进过程中,经常需要对数据结构进行重构。例如,某个对象类型新增了一些属性,但部分旧代码只需要旧的属性集合。使用 Pick 可以在不影响旧代码的情况下,轻松适配新的数据结构。 假设我们有一个旧的 User 类型:
type OldUser = {
    username: string;
    password: string;
};

function oldLogin(user: OldUser) {
    // 旧的登录逻辑
    console.log('Logging in with old user:', user);
}

后来,User 类型进行了扩展:

type NewUser = {
    username: string;
    password: string;
    email: string;
    phone: string;
};

为了让旧的 oldLogin 函数能够继续使用,同时不影响新的业务逻辑,可以使用 Pick

function oldLogin(user: Pick<NewUser, 'username' | 'password'>) {
    // 旧的登录逻辑
    console.log('Logging in with old user:', user);
}

这样,oldLogin 函数仍然可以接收新 NewUser 类型的部分属性,而不需要修改函数内部的逻辑。 2. 未来扩展性的考虑 当设计代码架构时,使用 Pick 可以为未来的扩展留下空间。例如,在定义组件时,如果使用 Pick 挑选出部分属性作为组件的 props,当源类型增加新属性时,组件不会受到直接影响,除非明确需要使用新属性。

type Post = {
    title: string;
    content: string;
    author: string;
    // 未来可能会增加更多属性,如发布时间、点赞数等
};

type PostPreviewProps = Pick<Post, 'title' | 'author'>;

const PostPreview: React.FC<PostPreviewProps> = ({ title, author }) => (
    <div>
        <h2>{title}</h2>
        <p>By {author}</p>
    </div>
);

即使未来 Post 类型增加了新属性,PostPreview 组件仍然可以正常工作,直到需要在预览中显示新属性时,才需要对 PostPreviewProps 进行更新。

九、注意事项与常见错误

  1. 键的大小写敏感 在 TypeScript 中,类型的键是大小写敏感的。在使用 Pick 时,确保挑选的键与源类型中的键大小写完全一致,否则会导致类型错误。
type Book = {
    Title: string;
    Author: string;
};

// 错误:键 'title' 与源类型中的 'Title' 大小写不一致
// type BookTitle = Pick<Book, 'title'>;

// 正确
type BookTitle = Pick<Book, 'Title'>;
  1. 类型兼容性问题 虽然 Pick 创建的新类型只包含部分属性,但它仍然与源类型存在一定的类型兼容性关系。在进行类型赋值或函数参数传递时,需要注意这种兼容性。例如,一个接受 Pick 类型参数的函数,不能直接传递源类型的对象,除非进行类型断言或使用更灵活的类型定义。
type Fruit = {
    name: string;
    color: string;
    taste: string;
};

type FruitNameColor = Pick<Fruit, 'name' | 'color'>;

function printFruitNameColor(fruit: FruitNameColor) {
    console.log(`Name: ${fruit.name}, Color: ${fruit.color}`);
}

const apple: Fruit = {
    name: 'Apple',
    color:'red',
    taste:'sweet'
};

// 错误:不能将 Fruit 类型直接赋值给 FruitNameColor 类型
// printFruitNameColor(apple);

// 正确:使用类型断言
printFruitNameColor(apple as FruitNameColor);
  1. 避免过度使用导致代码复杂 尽管 Pick 是一个强大的工具类型,但过度使用可能会使代码变得复杂和难以理解。特别是在多层嵌套使用 Pick 或与其他复杂工具类型组合时,要谨慎考虑代码的可读性和可维护性。尽量保持类型定义简洁明了,避免为了追求极致的类型安全而牺牲代码的可理解性。例如,在嵌套对象中多次使用 Pick 挑选属性时,可以适当提取中间类型,使代码结构更清晰。
// 复杂的多层 Pick 嵌套
type ComplexObject = {
    a: {
        b: {
            c: string;
            d: number;
        };
        e: boolean;
    };
};

// 不好的写法
// type NestedPick = Pick<ComplexObject, 'a'> & {
//     a: Pick<Pick<ComplexObject['a'], 'b'> & {
//         b: Pick<ComplexObject['a']['b'], 'c'>;
//     }, 'b'>;
// };

// 好的写法,提取中间类型
type InnerB = Pick<ComplexObject['a']['b'], 'c'>;
type InnerA = Pick<ComplexObject['a'], 'b'> & { b: InnerB };
type NestedPick = Pick<ComplexObject, 'a'> & { a: InnerA };

通过深入了解 Pick 工具类型的各种特性、应用场景以及注意事项,开发者能够在前端开发中更加灵活、高效地运用它,提升代码的质量和可维护性。无论是在简单的数据传输对象构建,还是复杂的组件属性管理和代码重构中,Pick 都能发挥重要作用。同时,合理地将 Pick 与其他工具类型组合使用,可以进一步挖掘 TypeScript 类型系统的强大功能,为项目的稳健发展提供有力支持。在日常编码过程中,不断实践和总结使用 Pick 的经验,有助于形成更加优雅和高效的代码风格。