美文网首页
TypeScript 详解之 TypeScript 泛型

TypeScript 详解之 TypeScript 泛型

作者: you的日常 | 来源:发表于2023-08-10 20:33 被阅读0次

    简介

    有些时候,函数返回值的类型与参数类型是相关的。

    function getFirst(arr) {
      return arr[0];
    }
    
    

    上面示例中,函数getFirst()总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。

    这个函数的类型声明只能写成下面这样。

    function f(arr:any[]):any {
      return arr[0];
    }
    
    

    上面的类型声明,就反映不出参数与返回值之间的类型关系。

    为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。

    function getFirst<T>(arr:T[]):T {
      return arr[0];
    }
    
    

    上面示例中,函数getFirst()的函数名后面尖括号的部分<T>,就是类型参数,参数要放在一对尖括号(<>)里面。本例只有一个类型参数T,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。

    上例的函数getFirst()的参数类型是T[],返回值类型是T,就清楚地表示了两者之间的关系。比如,输入的参数类型是number[],那么 T 的值就是number,因此返回值类型也是number

    函数调用时,需要提供类型参数。

    getFirst<number>([1, 2, 3])
    
    

    上面示例中,调用函数getFirst()时,需要在函数名后面使用尖括号,给出类型参数T的值,本例是<number>

    不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断。

    getFirst([1, 2, 3])
    
    

    上面示例中,TypeScript 会从实际参数[1, 2, 3],推断出类型参数 T 的值为number

    有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出了。

    function comb<T>(arr1:T[], arr2:T[]):T[] {
      return arr1.concat(arr2);
    }
    
    

    上面示例中,两个参数arr1arr2和返回值都是同一个类型。如果不给出类型参数的值,下面的调用会报错。

    comb([1, 2], ['a', 'b']) // 报错
    
    

    上面示例会报错,TypeScript 认为两个参数不是同一个类型。但是,如果类型参数是一个联合类型,就不会报错。

    comb<number|string>([1, 2], ['a', 'b']) // 正确
    
    

    上面示例中,类型参数是一个联合类型,使得两个参数都符合类型参数,就不报错了。这种情况下,类型参数是不能省略不写的。

    类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用T(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。

    下面是多个类型参数的例子。

    function map<T, U>(
      arr:T[],
      f:(arg:T) => U
    ):U[] {
      return arr.map(f);
    }
    
    // 用法实例
    map<string, number>(
      ['1', '2', '3'],
      (n) => parseInt(n)
    ); // 返回 [1, 2, 3]
    
    

    上面示例将数组的实例方法map()改写成全局函数,它有两个类型参数TU。含义是,原始数组的类型为T[],对该数组的每个成员执行一个处理函数f,将类型T转成类型U,那么就会得到一个类型为U[]的数组。

    总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。

    泛型的写法

    泛型主要用在四个场合:函数、接口、类和别名。

    函数的泛型写法

    上一节提到,function关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。

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

    那么对于变量形式定义的函数,泛型有下面两种写法。

    // 写法一
    let myId:<T>(arg:T) => T = id;
    
    // 写法二
    let myId:{ <T>(arg:T): T } = id;
    
    

    接口的泛型写法

    interface 也可以采用泛型的写法。

    interface Box<Type> {
      contents: Type;
    }
    
    let box:Box<string>;
    
    

    上面示例中,使用泛型接口时,需要给出类型参数的值(本例是string)。

    下面是另一个例子。

    interface Comparator<T> {
      compareTo(value:T): number;
    }
    
    class Rectangle implements Comparator<Rectangle> {
    
      compareTo(value:Rectangle): number {
        // ...
      }
    }
    
    

    上面示例中,先定义了一个泛型接口,然后将这个接口用于一个类。

    泛型接口还有第二种写法。

    interface Fn {
      <Type>(arg:Type): Type;
    }
    
    function id<Type>(arg:Type): Type {
      return arg;
    }
    
    let myId:Fn = id;
    
    

    上面示例中,Fn的类型参数Type的具体类型,需要函数id在使用时提供。所以,最后一行的赋值语句不需要给出Type的具体类型。

    此外,第二种写法还有一个差异之处。那就是它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。

    类的泛型写法

    泛型类的类型参数写在类名后面。

    class Pair<K, V> {
      key: K;
      value: V;
    }
    
    

    下面是继承泛型类的例子。

    class A<T> {
      value: T;
    }
    
    class B extends A<any> {
    }
    
    

    上面示例中,类A有一个类型参数T,使用时必须给出T的类型,所以类B继承时要写成A<any>

    泛型也可以用在类表达式。

    const Container = class<T> {
      constructor(private readonly data:T) {}
    };
    
    const a = new Container<boolean>(true);
    const b = new Container<number>(0);
    
    

    上面示例中,新建实例时,需要同时给出类型参数T和类参数data的值。

    下面是另一个例子。

    class C<NumType> {
      value!: NumType;
      add!: (x: NumType, y: NumType) => NumType;
    }
    
    let foo = new C<number>();
    
    foo.value = 0;
    foo.add = function (x, y) {
      return x + y;
    };
    
    

    上面示例中,先新建类C的实例foo,然后再定义示例的value属性和add()方法。类的定义中,属性和方法后面的感叹号是非空断言,告诉 TypeScript 它们都是非空的,后面会赋值。

    JavaScript 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。

    type MyClass<T> = new (...args: any[]) => T;
    
    // 或者
    interface MyClass<T> {
      new(...args: any[]): T;
    }
    
    // 用法实例
    function createInstance<T>(
      AnyClass: MyClass<T>,
      ...args: any[]
    ):T {
      return new AnyClass(...args);
    }
    
    

    上面示例中,函数createInstance()的第一个参数AnyClass是构造函数(也可以是一个类),它的类型是MyClass<T>,这里的TcreateInstance()的类型参数,在该函数调用时再指定具体类型。

    注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。

    class C<T> {
      static data: T;  // 报错
      constructor(public value:T) {}
    }
    
    

    上面示例中,静态属性data引用了类型参数T,这是不可以的,因为类型参数只能用于实例属性和实例方法,所以报错了。

    类型别名的泛型写法

    type 命令定义的类型别名,也可以使用泛型。

    type Nullable<T> = T | undefined | null;
    
    

    上面示例中,Nullable<T>是一个泛型,只要传入一个类型,就可以得到这个类型与undefinednull的一个联合类型。

    相关文章

      网友评论

          本文标题:TypeScript 详解之 TypeScript 泛型

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