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

TypeScript工具类型源码深度解读

2024-12-161.6k 阅读

1. 工具类型概述

在 TypeScript 的世界里,工具类型(Utility Types)就像是瑞士军刀一样,为开发者提供了便捷且强大的类型操作能力。这些工具类型在 lib.es5.d.ts 等标准库文件中定义,它们基于 TypeScript 的类型系统,对已有类型进行加工、转换,从而满足各种复杂的类型需求。工具类型使得我们能够在类型层面进行编程,大大增强了类型系统的表达能力。

2. 常用工具类型源码解析

2.1 Partial<T>

Partial<T> 的作用是将类型 T 的所有属性变为可选。它的源码实现非常简洁:

type Partial<T> = {
    [P in keyof T]?: T[P];
};

这里使用了索引类型和映射类型。keyof T 获取类型 T 的所有键,[P in keyof T] 则遍历这些键,? 表示将每个属性变为可选,T[P] 表示属性的值类型保持不变。

示例:

interface User {
    name: string;
    age: number;
}

let partialUser: Partial<User> = {}; // 合法,因为属性都变为可选
partialUser.name = 'John';
partialUser.age = 30;

2.2 Required<T>

Partial<T> 相反,Required<T> 把类型 T 的所有属性变为必选。源码如下:

type Required<T> = {
    [P in keyof T]-?: T[P];
};

这里 -? 表示移除属性的可选修饰符,从而使所有属性变为必选。

示例:

interface OptionalUser {
    name?: string;
    age?: number;
}

let requiredUser: Required<OptionalUser> = {name: 'Jane', age: 25}; // 必须提供所有属性

2.3 Readonly<T>

Readonly<T> 用于将类型 T 的所有属性变为只读。源码为:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

通过在属性定义前加上 readonly 关键字,实现了属性的只读特性。

示例:

interface MutablePoint {
    x: number;
    y: number;
}

let readonlyPoint: Readonly<MutablePoint> = {x: 10, y: 20};
// readonlyPoint.x = 30; // 错误,不能修改只读属性

2.4 Pick<T, K>

Pick<T, K> 从类型 T 中选择一组属性 K 来创建一个新类型。源码如下:

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

K extends keyof T 确保 KT 中存在的键。然后通过映射类型,从 T 中挑选出 K 对应的属性。

示例:

interface FullUser {
    name: string;
    age: number;
    email: string;
}

type BasicUser = Pick<FullUser, 'name' | 'age'>;
let basicUser: BasicUser = {name: 'Bob', age: 40};

2.5 Omit<T, K>

Omit<T, K>Pick<T, K> 相反,它从类型 T 中移除一组属性 K 来创建新类型。虽然标准库中没有直接的 Omit 定义,但可以通过其他工具类型组合实现:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

这里先通过 Exclude<keyof T, K> 排除 T 中属于 K 的键,然后再用 Pick 挑选剩余的键。

示例:

interface AllInfo {
    title: string;
    content: string;
    createdAt: Date;
}

type ContentOnly = Omit<AllInfo, 'createdAt'>;
let contentOnly: ContentOnly = {title: 'Article', content: 'This is the content'};

2.6 Exclude<T, U>

Exclude<T, U> 用于从类型 T 中排除可以赋值给类型 U 的类型。源码如下:

type Exclude<T, U> = T extends U? never : T;

这是一个条件类型,它检查 T 是否可以赋值给 U,如果可以则返回 never,否则返回 T

示例:

type AllNumbers = 1 | 2 | 3 | 4 | 5;
type EvenNumbers = 2 | 4;

type OddNumbers = Exclude<AllNumbers, EvenNumbers>; // 1 | 3 | 5

2.7 Extract<T, U>

Extract<T, U>Exclude<T, U> 相反,它从类型 T 中提取可以赋值给类型 U 的类型。源码为:

type Extract<T, U> = T extends U? T : never;

同样是条件类型,检查 T 是否可以赋值给 U,如果可以则返回 T,否则返回 never

示例:

type AllFruits = 'apple' | 'banana' | 'cherry' | 'date';
type SweetFruits = 'apple' | 'banana' | 'date';

type SelectedFruits = Extract<AllFruits, SweetFruits>; // 'apple' | 'banana' | 'date'

2.8 NonNullable<T>

NonNullable<T> 用于从类型 T 中排除 nullundefined。源码如下:

type NonNullable<T> = T extends null | undefined? never : T;

通过条件类型判断 T 是否为 nullundefined,如果是则返回 never,否则返回 T

示例:

type MaybeNumber = number | null | undefined;

type DefiniteNumber = NonNullable<MaybeNumber>; // number

2.9 ReturnType<T>

ReturnType<T> 用于获取函数类型 T 的返回值类型。源码如下:

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R? R : never;

这里使用了条件类型和类型推断(infer)。T extends (...args: any[]) => any 确保 T 是一个函数类型,然后通过 infer R 推断出函数的返回值类型 R

示例:

function add(a: number, b: number): number {
    return a + b;
}

type AddReturnType = ReturnType<typeof add>; // number

2.10 Parameters<T>

Parameters<T> 用于获取函数类型 T 的参数类型组成的元组类型。源码如下:

type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any? P : never;

同样利用条件类型和 infer,推断出函数 T 的参数类型 P

示例:

function greet(name: string, age: number) {
    return `Hello, ${name}! You are ${age} years old.`;
}

type GreetParams = Parameters<typeof greet>; // [string, number]

2.11 ConstructorParameters<T>

ConstructorParameters<T> 用于获取构造函数类型 T 的参数类型组成的元组类型。源码如下:

type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any? P : never;

Parameters 类似,不过这里限定 T 是构造函数类型(new (...args: any[]) => any)。

示例:

class Person {
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

type PersonConstructorParams = ConstructorParameters<typeof Person>; // [string, number]

2.12 InstanceType<T>

InstanceType<T> 用于获取构造函数类型 T 创建的实例类型。源码如下:

type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R? R : never;

通过条件类型和 infer,推断出构造函数 T 创建的实例类型 R

示例:

class Animal {
    constructor(public species: string) {}
}

type AnimalInstance = InstanceType<typeof Animal>; // Animal

2.13 ThisParameterType<T>

ThisParameterType<T> 用于获取函数类型 Tthis 参数类型,如果没有 this 参数则返回 unknown。源码如下:

type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any? U : unknown;

利用条件类型和 infer 推断 this 参数类型 U

示例:

function printInfo(this: {name: string}) {
    console.log(`Name: ${this.name}`);
}

type PrintInfoThisType = ThisParameterType<typeof printInfo>; // {name: string}

2.14 OmitThisParameter<T>

OmitThisParameter<T> 用于移除函数类型 Tthis 参数。源码如下:

type OmitThisParameter<T> = unknown extends ThisParameterType<T>? T : T extends (...args: infer A) => infer R? (...args: A) => R : T;

这里先判断函数是否有 this 参数,如果有则通过条件类型移除 this 参数。

示例:

function withThis(this: {id: number}, value: string) {
    return `ID: ${this.id}, Value: ${value}`;
}

type WithoutThis = OmitThisParameter<typeof withThis>;
// 相当于 (value: string) => string

2.15 ThisType<T>

ThisType<T> 用于在对象字面量类型中指定 this 的类型。它本身并不直接定义在标准库的工具类型中,而是在 tsconfig.json 开启 noImplicitThis 时,配合 @ts-this-type 使用。例如:

interface MyContext {
    data: string;
    printData(): void;
}

let context: MyContext & {[Symbol.hasInstance](x: any): x is MyContext} = {
    data: 'initial',
    printData() {
        console.log(this.data);
    }
};

function executeWithContext<T extends MyContext>(callback: (this: T) => void) {
    callback.call(context);
}

executeWithContext<MyContext>(function () {
    this.printData();
});

这里通过 (this: T) => void 明确了函数内部 this 的类型。

3. 工具类型的组合与高级应用

3.1 复杂类型构建

工具类型可以相互组合,构建出非常复杂的类型。例如,我们想创建一个只读且部分可选的用户类型:

interface UserProfile {
    name: string;
    age: number;
    address: string;
}

type ReadonlyPartialUser = Readonly<Partial<UserProfile>>;

let readonlyPartialUser: ReadonlyPartialUser = {name: 'Alice'};
// readonlyPartialUser.age = 28; // 错误,属性只读

3.2 泛型函数中的应用

在泛型函数中,工具类型能帮助我们更好地处理不同类型的参数和返回值。例如,创建一个函数,它接受一个对象和一组属性名,返回一个只包含指定属性的新对象:

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

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

let product: Product = {name: 'Widget', price: 10, description: 'A useful widget'};
let basicProduct = pickObject(product, ['name', 'price']);

3.3 条件类型的嵌套与逻辑

条件类型可以嵌套,实现更复杂的类型逻辑。比如,我们想定义一个类型,如果传入的类型是数组,则返回数组元素类型,否则返回原类型:

type ElementType<T> = T extends Array<infer U>? U : T;

type StringElementType = ElementType<string[]>; // string
type NumberElementType = ElementType<number>; // number

再比如,实现一个类型,如果类型 T 是函数类型,则返回其返回值类型,否则如果 T 是数组类型,则返回数组元素类型,否则返回 T 本身:

type GetType<T> = T extends (...args: any[]) => any? ReturnType<T> : T extends Array<infer U>? U : T;

function sum(a: number, b: number) {
    return a + b;
}

type SumReturnType = GetType<typeof sum>; // number
type StringArrayElementType = GetType<string[]>; // string
type BooleanType = GetType<boolean>; // boolean

4. 工具类型在实际项目中的价值

4.1 提高代码的可维护性

通过工具类型,我们可以更精确地定义类型,使得代码的意图更加清晰。例如,在一个大型项目中,可能有多个地方需要用到用户信息,但不同模块可能只需要部分用户属性。使用 Pick 等工具类型,可以清晰地定义出每个模块所需的用户类型,当用户信息结构发生变化时,只需修改一处,其他地方依赖的类型也会自动更新,大大提高了代码的可维护性。

4.2 增强代码的健壮性

工具类型能够在编译时发现更多类型错误。比如,使用 RequiredNonNullable 可以确保对象属性的完整性和非空性,避免运行时因为属性缺失或为 null/undefined 而导致的错误。在处理函数参数和返回值类型时,ParametersReturnType 等工具类型能保证函数调用的正确性,减少潜在的运行时错误。

4.3 提升开发效率

工具类型提供了便捷的类型操作方式,减少了手动编写类型的工作量。例如,当我们需要从一个已有类型中创建一个只读或部分可选的新类型时,使用 ReadonlyPartial 等工具类型只需一行代码,而手动编写则需要重复定义属性并添加修饰符,效率较低。同时,工具类型的组合使用可以快速构建出满足复杂业务需求的类型,加快开发进度。

5. 总结工具类型的要点与注意事项

5.1 要点总结

工具类型是 TypeScript 强大类型系统的重要组成部分,通过映射类型、条件类型和类型推断等机制,实现了对已有类型的灵活转换和操作。掌握常用工具类型如 PartialRequiredReadonlyPickOmit 等的原理和用法,以及它们之间的组合方式,能够极大地提升我们在类型层面编程的能力。条件类型中的 infer 关键字是推断类型的关键,在处理函数类型相关的工具类型如 ReturnTypeParameters 等时起着重要作用。

5.2 注意事项

在使用工具类型时,要注意类型的兼容性和边界情况。例如,在使用 ExcludeExtract 等条件类型时,要确保类型参数的正确性,否则可能得到不符合预期的结果。对于复杂的工具类型组合,可能会导致类型过于复杂难以理解,这时需要合理拆分和注释,提高代码的可读性。另外,不同版本的 TypeScript 对工具类型的支持和实现可能略有差异,在升级 TypeScript 版本时要注意相关的兼容性问题。

通过深入理解和熟练运用 TypeScript 的工具类型,我们能够编写出更健壮、可维护且高效的代码,充分发挥 TypeScript 类型系统的优势。无论是小型项目还是大型企业级应用,工具类型都能为我们的开发工作带来巨大的价值。