面向Type编程 -- Typescript类型和类型操作(四)

196 阅读7分钟

泛型及其应用

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。

泛型变量

看下面这个官网的例子,identity函数输入类型跟输出类型一致:

function identity(arg: number): number {
    return arg;
}

function identity(arg: string): string {
    return arg;
}

这种情况,可以使用any类型来定义函数。

function identity(arg: any): any {
    return arg;
}

这样做,会导致函数可以接受任何类型的参数,而且输入类型和输出类型一致的限制也会丢失。因此,我们需要一种能够在应用时动态传参,并生成目标函数的操作。 用类型参数T代替string和number,可以像使用函数一样,在应用时传入具体的类型参数:

function identity<T>(arg: T): T {
    return arg;
}

let output = identity<string>("myString")

这就是Typescript中的泛型,关于泛型,前面的文章我们已经使用过很多次。比如,前面介绍in操作符时,用到的实例。Pick和Partial中的T和K就是类型形参,而Person和'name'是类型实参。

type Person = {
    name: string;
    age: number;
    male: boolean;
}

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

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

type PickName = Pick<Person, 'name'>

// 结果
// {
//     name: string
// }

type PartialPerson = Partial<Person>

// 结果
// {
//     name?: string;
//     age?: number;
//     male?: boolean;
// }

泛型的应用技巧

Typescript语言中,泛型的应用很广泛,也很重要。某种意义上,对泛型的掌握熟练度,决定了对Typescript语言的掌握程度。只有熟练掌握和使用泛型,才能正确的使用Typescript语言。

泛型就是类型函数

泛型类型的声明和应用,跟函数的声明和应用相似。下面我们用一个实例,对比两种声明格式,直观感受一下二者之间的异同

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

//vs

function Pick(Obj, key){
    return Obj[ky]
}

二者之间有很多一一对应的地方:

  • 关键字type对应function
  • 界限符<>对应()
  • 类型名称对应函数名称
  • 同样大括号界限操作执行体
  • 应用传参执行的过程同样极其相似

区别在于,泛型声明的是类型,入参和执行结果同样也是类型,是对类型的编程;函数声明入参和执行结果都是,是对的编程。

掌握类型编程,关键要完成值操作到类型操作的思维转换。类型编程思维一旦建立,对typescript的认知和理解会踏上一个新的台阶。下面我通过了解泛型的各种编程操作,进一步系统的认识一下泛型。

类型的条件判断

前面我们讨论extends操作符的时候,已经使用过条件判断。应用模型是:

T extends U ? X : Y

条件判断表达式类似于三元运算。其含义表示,如果T包含的类型是U包含的类型的 ‘子集’,那么取结果X,否则取结果Y。条件判断的应用场景很广泛,例如约束、过滤、匹配提取等。

下面看几个实例:

// 去除null和undefined类型
type NonNullable<T> = T extends null | undefined ? never : T

// 保留指定类型
type Extract<T, U> = T extends U ? T : never

type OnlyString<T> = Extract<T, string> 

type Result = OnlyString<'a' | 1 | 'b' | false> // R = 'a'|'b'

// 过滤掉指定类型
type Exclude<T,U> = T extends U ? never : T

type NonString<T> = Exclude<T, string> 

type Result1 = NonString<'a' | 1 | 'b' | false> // R = 1 | false

类型的遍历及映射

基于in操作符结合[]可以遍历联合类型的特性,可以对类型成员进行遍历操作。首先利用keyof操作符,取出类型成员属性的联合类型,然后进行遍历操作。

前面讨论结合in操作符时,讨论过类型的遍历操作,可以把指定类型成员变成可选。(同样是上面”泛型类型”中讨论过的例子)

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

type Person = {
    name: string;
    age: number;
    male: boolean;
}

type PartialPerson = Partial<Person>

// 结果
// {
//     name?: string;
//     age?: number;
//     male?: boolean;
// }

基于in操作符结合[]可以遍历联合类型的特性,我们还可以做更多的事情。

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

type ReadonlyPerson = Readonly<Person>

// 结果
// {
//     readonly name: string;
//     readonly age: number;
//     readonly male: boolean;
// }

还可以结合操作符"-"对上面映射类型,进行反向操作

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

type Person1 = Required<PartialPerson>

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

type Person2 = UnReadOnly<ReadonlyPerson>

泛型递归

泛型操作支持递归,看下面例子,DeepReadonly是上面Readonly的加强,深度递归遍历把类型成员全部转化成readonly,

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

type Person = {
    name: string;
    age: number;
    male: boolean;
    child:{
        name:string;
        scholl:string;
    }
}

type DeepReadonlyPerson = DeepReadonly<Person3>

type ReadOnlyChild = DeepReadonlyPerson['child']

// 结果
// {
//     readonly name: string;
//     readonly scholl: string;
// }
  • 元组递归循环

同样,递归适用于适用于元组。比如上面的泛型DeepReadonly,同样适用于元组。

type Tuple = [string, number, [boolean]]
type ReadonlyTuple = DeepReadonly<Tuple> // readonly [string, number, readonly [boolean]]

数组中除了forEach和for...in遍历以外,还有一种遍历数组的方式叫做递归。同样,我们可以像递归遍历数组那样,递归遍历元组。下面实例,我们递归循环出元组中的类型元素,转化成一个联合类型。

type Tuple = [string, number, boolean]
type Head<T extends any[]> = T[0]
type Union<T, U> = T | U
type Recursion<T extends any[], E = never> = {
    1: E,
    0: Recursion<ArrayShift<T>, Union<E, Head<T>>>
}[T extends [] ? 1 : 0]

type UnionTuple = Recursion<Tuple> // string | number | boolean

当然,元组递归的应用不止于此。建议记住并深刻理解这个元组递归模型,基于这个模型的拓展,我们能够创造出更丰富多彩的泛型工具。后面我们会一一介绍并使用。\

类型提取

应用中,很多时候,需要提取一个类型结构中,指定位置元素的类型。比如,泛型函数参数位置的类型,或者其返回值的类型、元组中第一个元素的类型等。上述都可以通过前面一章讲到的infer操作符结合extends判断得以实现。下面看一下具体实现代码,并通过使用这些泛型,创建几个实例,从而加深理解。

  • 函数参数类型推断提取

函数参数类型推断提取实现代码相对比较简单,首先通过 any>对泛型参数T进行约束,必须是类型函数,最后通过infer应用推导出参数类型P。

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

let addFn = (a:number,b:number):number=>a+b
type ParamType = Parameters<typeof addFn> // [a: number, b: number]

进一步思考,我们可以结合函数rest特性,实现第一个参数类型以及剩余参数类型的提取。下面是实现代码和实例。

type Head<T extends (...args: any) => any> = T extends (head:infer U, ...rest: any[]) => any ? U : never;

type Tail<T extends (...args: any) => any> = T extends (head: any, ...tail: infer U) => any ? U : never;

let addFn = (a: number, b: number): number => a + b
type HeadParamType = Head<typeof addFn> // number
type TailParamType = Tail<typeof addFn> // [number]

继续延伸思考,略微改造一下,同样可以实现类构造函数的参数提取

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

// 普通类
class Person {
    private name: string;
    private age: number;

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

type CPType = ConstructorParameters<typeof Person> // [name:string,age:number]

// 类型接口
interface GenericClass {
    new (name:string, age:number);
}

type GCParams = ConstructorParameters<GenericClass> // [name:string,age:number]
  • 函数返回值类型提取
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

let addFn = (a: number, b: number): number => a + b

type FnReturn = ReturnType<typeof addFn> // number

上面显性的指明了返回值类型,那么隐性的返回值类型推断提取是否可以实现呢?答案是肯定的。看下面实例,我们不显性指明函数返回值,并且增加条件判断,分别返回不同类型的结果。验证一下结果是否如我们期望的那样?

let fn = (arg: string | number) => {
    if (arg === 1) {
        return Number(arg)
    } else {
        return !!arg
    }
}

type FnReturn = ReturnType<typeof fn> // number | boolean

基于同样的原来,可以实现类实例的类型推断提取

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

class Person {
    private name: string;
    private age: number;

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

type TInstance = InstanceType<typeof Person>

类型及其成员的筛选

除了类型遍历映射以及类型提取以外,类型成员的筛选应用同样比较频繁。掌握类型成员的筛选技巧,变得尤为重要。相关的操作包括去除指定成员,保留指定成员等。基本实现原理,都是基于联合类型和extends判断,实现筛选的目的。

  • 联合类型的去除
type Exclude<T, U> = T extends U ? never : T;

type Keys = 'name' | 'age' | 'male'

type ExcludeMale = Exclude<Keys, 'male'> // "name" | "age"

如果你已经充分掌握,前面讨论的extends以及分布式联合类型。相信上面的演变过程已经非常熟悉。

Exclude<Keys, 'male'>
//=>
('name' | 'age' | 'male') extends 'male' ? never:T
//=>
('name' extends 'male' ? never:'name') | ('age' extends 'male' ? never:'age') | ('male' extends 'male' ? never:'male')
//=>
'name' | 'age' | never
//=>
'name' | 'age'
  • 联合类型的保留

如果把上面的Exclude中的T和never互换一下位置,将得到相反的结果。相应的【去除】变成【保留】。

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

type Keys = 'name' | 'age' | 'male'

type Extract = Extract<Keys, 'name'|'age'> // "name" | "age"
  • 类型成员的保留

只需要遍历,予保留的key即可

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

interface Person {
    name: string;
    age: number;
    work: string;
}

type NameAndAge = Pick<Person, 'name'|'age'>
  • 类型成员的去除

利用Exclude排除掉不予保留的key即可

type Remove<T,K extends keyof T> = Pick<T, Exclude<keyof T,K>>

interface Person {
    name: string;
    age: number;
    work: string;
}

type NoWork = Remove<Person,'work'> // {name:string;age:number}

参考资料:

github.com/millsp/ts-t…

stackoverflow.com/questions/5…

mariusschulz.com/blog/series…

juejin.cn/post/684490…