泛型_TypeScript笔记6

一.存在意义

考虑这样一个场景,identity函数接受一个参数,并原样返回:

function identity(arg) {
  return arg;
}

从类型上看,无论参数是什么类型,返回值的类型都与参数一致,借助重载机制,可以这样描述:

function identity(arg: number): number;
function identity(arg: string): string;
function identity(arg: boolean): boolean;
// ...等无数个 a => a 的类型描述

重载似乎并不能满足这个场景,因为我们没有办法穷举arg的所有可能类型。既然参数是任意类型,不妨用any试试:

function identity(arg: any): any;

覆盖到了所有类型,却丢失了参数与返回值的类型对应关系(上面相当于A => B的类型映射,而我们想要描述的是A => A

泛型与any

那么,应该如何表达两个any之间的对应关系呢?

用泛型。这样描述:

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

类型变量Tany类似,相当于具名any,这样就能明确表达T => T(即A => A)的类型映射了

二.类型变量

Type variable, a special kind of variable that works on types rather than values.

普通变量代表一个值,而类型变量代表一个类型

从作用上看,变量能够搬运值,而类型变量搬运的是类型信息:

This allows us to traffic that type information in one side of the function and out the other.

三.泛型函数

类型变量也叫类型参数,与函数参数类似,区别在于函数参数接受一个具体值,而类型参数接受一个具体类型,例如:

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

// 传参给类型参数
// identity<number>
// 传参给函数参数(自动推断类型参数)
identity(1);
// 传参给函数参数(显式传入类型参数)
identity<number>(1);

带有类型参数的函数称为泛型函数,其中类型参数代表任意类型(any and all types),所以只有所有类型共有的特征才能访问:

function loggingIdentity<T>(arg: T): T {
  // 报错 Property 'length' does not exist on type 'T'.
  console.log(arg.length);
  return arg;
}

实际上,因为有void这个空集在,所以并不存在所有类型通用的属性或方法。也不能对类型变量做任何假设(比如假定它有length属性),因为它代表一个任意类型,没有任何约束

除此之外,类型变量T就像一个具体类型一样,可以用于任何具体类型出没的地方:

function loggingIdentity<T>(arg: T[]): T[] {
  console.log(arg.length);  // Array has a .length, so no more error
  return arg;
}
// 或者
function loggingIdentity<T>(arg: Array<T>): Array<T> {
  console.log(arg.length);  // Array has a .length, so no more error
  return arg;
}

类型描述

泛型函数的类型描述与普通函数类似:

// 普通函数
let myIdentity: (arg: string) => string =
  function(arg: string): string {
    return arg;
  };
// 泛型函数
let myIdentity: <T>(arg: T) => T =
  function<T>(arg: T): T {
    return arg;
  };

仍然是箭头函数语法,只是在(参数列表)前增加了<类型参数列表>。同样的,类型描述中类型参数名也可以与实际的不一致:

let myIdentity: <U>(arg: U) => U =
  function<T>(arg: T): T {
    return arg;
  };

P.S.特殊的,函数类型描述还可以写成对象字面量的形式:

// 泛型函数
let myIdentity: { <T>(arg: T): T };
// 普通函数
let myIdentity: { (arg: string): string };

像是接口形式类型描述的退化版本,没有复用优势,也不如箭头函数简洁,因此,并不常见

四.泛型接口

带类型参数的接口叫泛型接口,例如可以用接口来描述一个泛型函数:

interface GenericIdentityFn {
  <T>(arg: T): T;
}

还有一种非常相像的形式:

interface GenericIdentityFn<T> {
  (arg: T): T;
}

这两种都叫泛型接口,区别在于后者的类型参数T作用于整个接口,例如:

interface GenericIdentity<T> {
  id(arg: T): T;
  idArray(...args: T[]): T[];
}
let id: GenericIdentity<string> = {
  id: (s: string) => s,
  // 报错 Types of parameters 's' and 'args' are incompatible.
  idArray: (...s: number[]) => s,
};

接口级的类型参数有这种约束作用,成员级的则没有(仅作用于该泛型成员)

五.泛型类

同样,带类型参数的类叫泛型类,例如:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

像接口一样,泛型类能够约束该类所有成员关注的目标类型一致:

Putting the type parameter on the class itself lets us make sure all of the properties of the class are working with the same type.

注意,类型参数仅适用于类中的实例成员,静态成员无法使用类型参数,例如:

class GenericNumber<T> {
  // 报错 Static members cannot reference class type parameters.
  static zeroValue: T;
}

因为静态成员在类实例间共享,无法唯一确定类型参数的具体类型:

let n1: GenericNumber<string>;
// 期望 n1.constructor.zeroValue 是 string
let n2: GenericNumber<number>;
// 期望 n1.constructor.zeroValue 是 number,出现矛盾

P.S.这一点与Java一致,具体见Static method in a generic class?

六.泛型约束

类型参数太“泛”(any and all)了,在一些场景下,可能想要加以约束,例如:

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);  // Now we know it has a .length property, so no more error
  return arg;
}

通过接口来描述对类型参数的约束T extends constraintInterface),比如上面要求类型参数T必须具有一个number类型的length属性`

另一个典型的场景是工厂方法,例如:

// 要求构造函数c必须返回同一类(或子类)的实例
function create<T>(c: {new(): T; }): T {
  return new c();
}

此外,还可以在泛型约束中使用类型参数,例如:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

能够用一个类型参数的特征去约束另一个类型参数,相当强大

七.总结

之所以叫泛型,是因为能够作用于一系列类型,是在具体类型之上的一层抽象:

Generics are able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code