美文网首页Web前端之路让前端飞
TypeScript中的泛型以及条件类型中的推断

TypeScript中的泛型以及条件类型中的推断

作者: 淺時咣 | 来源:发表于2020-11-13 11:16 被阅读0次

    泛型

    TypeScript 中泛型设计的目的是使在成员之间提供有意义的约束,为代码增加抽象层和提升可重用性。泛型可以应用于 Typescript 中的函数(函数参数、函数返回值)、接口和类(类的实例成员、类的方法)。

    简单示例

    先来看这个如果平常我们写函数的参数和返回值类型可能会这么写~约束了函数参数和返回值必须为数字类型。

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

    那么问题来了。如果我要参数和返回值类型限定为字符串类型的话,又改成这么写。

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

    不科学呀!当函数想支持多类型参数或返回值的时候,上述写法将变得十分不灵活。于是泛型就闪亮登场了!

    考虑以下写法:

    function identity<T>(arg: T): T {
      return arg;
    }
    
    function identities<T, U>(arg1: T, arg2: U): [T, U] {
      return [arg1, arg2];
    }
    

    使用泛型后,可以接受任意类型,但是又完成了函数参数和返回值的约束关系。十分灵活~可复用性大大增强了!

    泛型约束

    有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 给泛型加上约束。

    interface ILengthwise {
      length: number;
    }
    
    function loggingIdentity<T extends ILengthwise>(arg: T): T {
      console.log(arg.length);
      return arg;
    }
    

    其实泛型我们在 React 组件里也很常见(说不定大家觉得很眼熟了),用泛型确保了 React 组件的 Props 和 State 是类型安全的~

    interface ICustomToolProps {
      // @TODO
    }
    
    interface ICustomToolState {
      // @TODO
    }
    
    class CustomTool extends React.Component<ICustomToolProps, ICustomToolState> {
      // @TODO
    }
    

    所以大家看上面的 ICustomToolProps、ICustomToolState 其实也是泛型。应用在类上面的泛型语法简化如下示例:

    class Directive<T> {
      private name: T;
      public getName(): T {
        return this.name;
      }
      // @TODO
    }
    

    当使用泛型时,一般情况下常用 T、U、V 表示,如果比较复杂,应使用更优语义化的描述,比如上述 React 组件示例。

    实践一下

    比如说设计一个指令管理者对象~用来管理指令

    enum EDirective {
      Walk = 1,
      Jump = 2,
      Smile = 3
    }
    class DirectiveManager<T> {
      private directives: Array<T> = [];
      add = (directive: T): Array<T> => {
        this.directives = this.directives.concat(directive);
        return this.directives;
      };
      get = (index: number): T => {
        return this.directives[index];
      };
      shift = (): Array<T> => {
        this.directives = this.directives.slice(1);
        return this.directives;
      };
      // @TODO
    }
    

    初始化一个指令管理者的实例。给定泛型为 number 类型。


    微信图片_20201113105046.jpg

    可以发现指令管理者对象成功被限定类型,如果传参类型错误,会被 TypeScript 及时提醒。

    了解数组方法的泛型

    经过上面的介绍,相信大家都对泛型有一定了解了!那么接下来通过带大家看 JavaScript 数组方法的泛型来加深理解~

    我们来阅读以下数组对象的属性以及方法的泛型(我抽取了一部分,希望大家不要觉得代码过长,就略过不读,我觉得也是换一种方式熟悉 JavaScript 语法的一种方式~)

    interface Array<T> {
      length: number;
      [n: number]: T;
    
      reverse(): T[];
    
      shift(): T;
      pop(): T;
    
      unshift(...items: T[]): number;
      push(...items: T[]): number;
    
      slice(start?: number, end?: number): T[];
      sort(compareFn?: (a: T, b: T) => number): T[];
      indexOf(searchElement: T, fromIndex?: number): number;
      lastIndexOf(searchElement: T, fromIndex?: number): number;
      every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
      some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
      forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
      map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
      filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[];
    
      splice(start: number): T[];
      splice(start: number, deleteCount: number, ...items: T[]): T[];
    
      concat<U extends T[]>(...items: U[]): T[];
      concat(...items: T[]): T[];
    
      reduce(
        callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T,
        initialValue?: T
      ): T;
      reduce<U>(
        callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U,
        initialValue: U
      ): U;
    
      reduceRight(
        callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T,
        initialValue?: T
      ): T;
      reduceRight<U>(
        callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U,
        initialValue: U
      ): U;
    }
    

    相信大家对数组方法都十分熟悉了~下面将带大家稍微看一下部分方法

    shift/pop & push/unshift

    shift(): T;
    pop(): T;
    
    unshift(...items: T[]): number;
    push(...items: T[]): number;
    

    平时大家可能会混淆几个方法。但是看了它们的函数签名后,是否觉得一目了然。push/unshift 方法调用后返回时数字类型,也就是其数组长度。而 shift/pop 方法调用后返回了弹出的元素,

    forEach & map

    forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
    map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
    

    这两个方法很值得一说,因为两者都具备遍历的特征,所以常见很多同学们混用这两个方法,其实大有讲究。看到 forEach 的方法其实是返回 void 的,而在map 方法里,最终是将 T[] 映射成了 U[]。所以呢,一言以蔽之,forEach 一般用来执行副作用的,比如持久的修改一下元素、数组、状态等,以及打印日志等,本质上是不纯的。而 map 方法用来作为值的映射,本质上是纯净的,在函数式编程里十分重要。

    concat

    spliceconcatreducereduceRight 这些方法基本都重载了两次,也就明显告诉我们这些方法是有多种传参调用方式的。

    比如concat<U extends T[]>(...items: U[]): T[];这里使用到了上述和大家介绍的泛型约束,意思为可以传递多个数组元素。下面紧跟着的concat(...items: T[]): T[];则告诉我们也可以传递多个元素。两个函数签名都告诉我们函数返回一个数组,它由被调用的对象中的元素组成,每个参数的顺序依次是该参数的元素(如果参数是数组)或参数本身(如果参数不是数组)。它不会递归到嵌套数组参数中。

    映射类型

    有时候我们有从旧类型中创建新类型的一个需求场景,TypeScript 提供了映射类型这种方式。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性

    比如我们将每个属性成为 readonly 类型,如下

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

    同理如下,见图可理解~

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

    那么大家应该也 get 到下述代码的意图了~

    type Nullable<T> = { [P in keyof T]: T[P] | null };
    

    扩展一下可以写任意的映射类型来满足自己的需求场景~

    enum EDirective {
      Walk = 1,
      Jump = 2,
      Smile = 3
    }
    type DirectiveKeys = keyof typeof EDirective;
    type Flags = { [K in DirectiveKeys]: boolean };
    
    4.jpg
    type Pick<T, K extends keyof T> = { [P in K]: T[P] };
    type Record<K extends string, T> = { [P in K]: T };
    

    条件类型中的推断

    infer 表示在 extends 条件语句中待推断的类型变量。

    在条件类型的 extends 语句中,我们可以用 infer 声明一个类型变量,然后在其分支语句中使用该类型变量。如果不懂,没有关系,请继续看下面的例子~

    提取函数参数 & 提取函数返回值

    该语句中的(param: infer P),为函数首个参数推断声明了一个类型变量 P,如果泛型 T 是一个函数,则根据之前的类型变量 P,提取其推断的函数参数并返回,否则返回原有类型。

    type ParamType<T> = T extends (param: infer P) => any ? P : T;
    
    5.jpg

    如图所以,成功提取了 IPrint 的参数类型。

    同理如下,提取返回值同样理解~

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

    提取构造函数参数类型 & 提取实例类型

    下述代码可以提取构造函数参数类型~

    type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (
      ...args: infer P
    ) => any
      ? P
      : never;
    
    6.jpg
    T extends new (...args: any[]) => any这里用到了泛型约束,new (...args: infer P)这一句将参数推断声明为类型变量 P。剩余的还是一样的理解~

    下述提取实例类型(和提取构造函数参数类型小有不同同学们自己发现一下)

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

    其他常用的条件推断

    剩余的列举一些比较实用的,参照上述方式理解,同学们如若感兴趣,可自行谷歌~

    提取数组子元素

    type Flatten<T> = T extends (infer U)[] ? U : T;
    

    提取Promise值

    type Unpromisify<T> = T extends Promise<infer R> ? R : T;
    

    Tuple 转 Union

    type ElementOf<T> = T extends Array<infer E> ? E : never;
    

    Union 转 Intersection

    type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((
      k: infer I
    ) => void)
      ? I
      : never;
    

    什么时候使用泛型

    1.当函数、接口、类是接受多类型参数的时候,可以用泛型提高可重用性。
    2.当函数、接口、类需要在多个地方用到某个类型的时候。

    相关文章

      网友评论

        本文标题:TypeScript中的泛型以及条件类型中的推断

        本文链接:https://www.haomeiwen.com/subject/oudvbktx.html