TypeScript技术系列12:如何构建强大的工具类型?

763 阅读9分钟

前言

随着TypeScript在大型项目中应用的日益广泛,开发者对类型系统的要求不断提高。通常,内置工具类型如PartialPick等可以帮助简化类型的定义,但当需求更复杂时,往往需要构建自定义的工具类型。这些类型不仅能减少重复代码,提高代码的健壮性,还能帮助保持类型系统的灵活性。本篇文章将深入探讨TypeScript的工具类型,包括它们的基础概念、核心语法、以及如何构建实用的自定义工具类型,并通过实际示例加深理解,帮助我们在项目中有效运用工具类型。

1. 核心类型工具和语法

在构建TypeScript工具类型之前,首先需要了解TypeScript的核心类型工具和语法。这些基础概念构成了工具类型的核心逻辑,让我们可以对类型进行精细化的控制。以下是8个常见的核心类型工具和语法:

1.1 泛型

泛型TypeScript中的核心概念之一,它允许编写可以接受不同类型参数的函数、接口、类等工具类型。通过泛型,可以实现类型的灵活和复用。示例代码如下:

type isSubTyping<Child, Parent> = Child extends Parent ? true : false;

type isXX = isSubTyping<1, number>;    // true
type isYY = isSubTyping<'string', string>; // true
type isZZ = isSubTyping<true, boolean>;    // true

在上述代码中,isSubTyping类型接受两个类型参数ChildParent,并使用extends来判断Child是否可以赋值给 Parent。如果可以,返回true,否则返回false。在这个示例中,1number的子类型,因此isXX的结果是true

1.2 条件类型

条件类型能够根据某个类型的条件判断来决定返回的类型,形式类似于JavaScript中的三元运算符。通过条件类型,可以基于类型关系作出动态判断,示例代码如下:

type isAssertable<T, S> = T extends S ? true : S extends T ? true : false;

type isNumAssertable = isAssertable<1, number>; // true
type isStrAssertable = isAssertable<string, 'string'>; // true
type isNotAssertable = isAssertable<1, boolean>; // false

在上述代码中,isAssertable类型通过条件运算符检查TS之间的可断言关系。如果T可以赋值给S,或者S可以赋值给T,则返回true,否则返回false。例如,1可以赋值给number,所以isNumAssertable的结果为true

1.3 分配条件类型

分配条件类型可以将联合类型中的每个成员单独提取出来进行条件判断。这对于处理联合类型时非常有用,示例代码如下:

type BooleanOrString = string | boolean;
type StringOrNumberArray<E> = E extends string | number ? E[] : E;

type WhatIsThis = StringOrNumberArray<BooleanOrString>; // boolean | string[]

在上述代码中,StringOrNumberArray是一个条件类型,它检查E是否为stringnumber类型。如果是,则返回E[]类型;否则,返回原始类型。此处,BooleanOrString是联合类型,StringOrNumberArray会分别对每个成员进行处理,最终返回boolean | string[]

1.4 never类型

never类型TypeScript中的一个特殊类型,它是所有类型的子类型,但它不能被赋值或推断为其他类型。在条件类型中使用never类型时,要特别小心,示例代码如下:

type GetNever = StringOrNumberArray<never>; // never

在上述代码中,never类型不会与其他类型匹配,因此StringOrNumberArray<never>的结果为never。这意味着如果条件类型的输入是never,则不会返回任何有效的类型。

1.5 infer

infer关键字允许在条件类型中提取子类型的具体类型。它常用于从类型中提取出某一部分或推断类型,示例代码如下:

type ElementTypeOfArray<T> = T extends (infer E)[] ? E : never;

type isNumber = ElementTypeOfArray<number[]>; // number
type isNever = ElementTypeOfArray<number>;    // never

在上述代码中,ElementTypeOfArray类型使用infer来推断数组类型中的元素类型。如果T是一个数组类型,infer E会提取出数组中的元素类型;否则,返回never类型。在示例中,number[]的元素类型是number,所以isNumber结果为number

1.6 keyof

索引访问类型允许我们从对象类型中提取某个属性的类型。keyof操作符用于获取对象类型的所有属性名称,形成一个联合类型,示例代码如下:

interface MixedObject {
  animal: { type: 'dog' | 'cat'; age: number };
  [key: number]: { type: string; age: number; nickname: string };
}

type animal = MixedObject['animal'];
type animalType = MixedObject['animal']['type'];
type numberIndex = MixedObject[number];

在上述代码中,通过MixedObject['animal']可以访问到animal属性的类型;MixedObject['animal']['type']获取到animal属性中的type类型。keyof MixedObject获取的是所有属性名的联合类型:"animal" | number

1.7 typeof 操作符

typeof操作符可以在表达式中获取一个变量的类型,也可以用于从现有的对象中提取类型,示例代码如下:

const str = "Hello, TypeScript";
type StrType = typeof str; // string

const animal = { id: 1, name: 'animal' };
type Animal = typeof animal; // { id: number; name: string }

在上述代码中,typeof用于获取变量或对象的类型。在第一个例子中,typeof str获取到str变量的类型string。在第二个例子中,typeof animal提取了animal对象的类型,返回{ id: number; name: string }

1.8 映射类型

映射类型允许根据给定的类型模板创建新的类型。通过in关键字,可以遍历对象类型的每个属性,并对其进行操作。还可以结合keyof操作符,基于已有类型创建新的类型,示例代码如下:

type SpecifiedKeys = 'id' | 'name';
type TargetType = { [key in SpecifiedKeys]: any }; // { id: any; name: any }

interface SourceInterface {
  readonly id: number;
  name?: string;
}

type TargetTypeFromSource = {
  [key in keyof SourceInterface]: SourceInterface[key];
}; // { readonly id: number; name?: string }

在第一个例子中,定义了一个映射类型TargetType,它包含idname属性,类型为any。在第二个例子中,TargetTypeFromSource类型通过遍历SourceInterface的所有属性(使用keyof SourceInterface)并保持原始属性类型来创建新类型。

2. 构建常见的工具类型

下面将展示如何构建5个常见的自定义工具类型,并包含更详细的解释和示例代码。

2.1类型过滤工具类型ExcludeType

ExcludeType是一个用于剔除类型联合中的特定类型的工具类型。它常用于从联合类型中移除不需要的类型成员,示例如下:

type ExcludeType<T, U> = T extends U ? never : T;
type ExampleExclude1 = ExcludeType<string | number | boolean, string>; // number | boolean

在这个示例中,ExcludeType使用条件类型判断T是否为U的子类型,是则返回never,否则保留该类型。

2.2 类型属性转换工具类型Readonlyify

以下是实现一个将对象所有属性都设置为只读的工具类型Readonlyify。它适用于需要将一个类型的所有字段设为不可变的场景,示例如下:

type Readonlyify<T> = {
    readonly [K in keyof T]: T[K];
};

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

type ReadonlyUser = Readonlyify<User>;
// ReadonlyUser 等同于 { readonly id: number; readonly name: string; }

Readonlyify使用映射类型将T中每个属性都转换为只读。

2.3 获取函数参数类型的工具类型ParameterType

ParameterType是一个用于提取函数参数类型的工具类型。它在类型定义中允许对函数参数进行类型推导,示例如下:

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

type FunctionParams = ParameterType<(x: number, y: string) => void>; 
// FunctionParams 为 [number, string]

这里的infer P提取了函数的参数类型,如果T是函数类型,则返回参数类型组成的元组类型,否则返回never

2.4 提取实例类型的工具类型InstanceTypeOf

InstanceTypeOf是用于从构造函数类型提取其实例的类型,示例如下:

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

class Person {
    constructor(public name: string) {}
}

type PersonInstance = InstanceTypeOf<typeof Person>; // Person

在这个示例中,InstanceTypeOf使用infer关键字提取构造函数的返回值类型,如果T为构造函数类型,则返回其实例类型。

2.5 深度递归工具类型DeepPartial

有时需要将对象的所有属性以及嵌套对象属性都设为可选,这时可以使用递归工具类型来实现。DeepPartial即为一种递归地将类型中的所有属性设为可选的工具类型,示例如下:

type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface ComplexObject {
    name: string;
    details: {
        age: number;
        address: string;
    };
}

type PartialComplexObject = DeepPartial<ComplexObject>;
// PartialComplexObject 的结构为 { name?: string; details?: { age?: number; address?: string } }

DeepPartial利用了条件类型和递归映射类型,确保将T中的每个属性(包括嵌套对象的属性)都转换为可选。

3. 应用场景和最佳实践

自定义工具类型在TypeScript项目中可以极大提高类型系统的灵活性和代码的复用性。以下是一些实用的建议:

  1. 保持工具类型的通用性和简洁性:避免将复杂的业务逻辑写入工具类型,确保工具类型适用于不同场景。
  2. 优先使用类型组合:在构建工具类型时,尽量将条件类型和infer等特性结合使用,以提升代码的表达力。
  3. 尽量避免深度递归:在使用递归工具类型时要注意其潜在的性能问题,确保递归深度在合理范围内。

总结

工具类型是TypeScript强大类型系统中的精髓所在,它们让开发者能够灵活地操控、转换和组合类型。通过掌握工具类型的创建方法,我们不仅能提升代码的安全性,还能降低冗余,提高可读性。这篇文章介绍了从基础的 ExcludeType、Readonlyify到更复杂的DeepPartial等常见工具类型,每个类型背后都蕴含着对泛型、条件类型、递归和infer关键字的深入应用。

编写自定义工具类型是一项值得投资的技能,它让我们不仅成为TypeScript的使用者,更是它的设计者。未来,随着TypeScript类型系统的发展,理解和应用这些技巧将帮助我们更加从容地应对复杂类型需求,为项目注入更高的灵活性和维护性。无论是对大型团队协作项目,还是对个人代码的健壮性,这些工具类型都将是不可或缺的利器。

后语

小伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走吧^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。