一、概述

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

先来看一组示例,定义一个函数,传入任意类型的数据并将其返回,你可能会想到使用 any,大致如下:

  1. function itself(arg: any): any {
  2. return arg;
  3. }

使用 any 类型会导致这个函数可以接收任何类型的 arg 参数,这样就丢失了一些信息,无法保证传入的类型与返回的类型是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回,如果传递的是一个数值类型的数据,那么返回值的类型也应该是一个数值类型,而使用 any,返回值为字符串类型的数据也能通过代码检测,那就无法满足我们此时需求。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

  1. function itself<T>(arg: T): T {
  2. return arg;
  3. }

我们给 itself 添加了类型变量 TT 帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 然后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的 itself 函数叫做泛型函数,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,传入什么类型就返回什么类型。

定义好泛型之后,有两种方法可以使用:

1)明确指定泛型类型

let name = itself<string>("myString");

2)类型推断

let name = itself("myString");

注意:我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看 myString 的值,然后把 T 设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。

二、泛型变量

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

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

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

现在假设我们想操作 T 类型的数组,所以 .length 属性肯定是存在的。 我们可以像创建其它数组一样创建这个数组:

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

你可以这么理解这段代码:函数 itself,接受一个 泛型 T 和一个参数 arg,其中,参数 arg 的类型 和 返回值类型为 元素类型为 T 的数组。所以,数组元素的类型具体是什么,取决于泛型变量 T 的值,如果你传入的是 number,比如:

itself<number>([1, 2, 3]);

则相当于 itself 的类型变成了如下形式:

function itself<number>(arg: number[]): number[]

这可以让我们把泛型变量 T 当做类型的一部分使用,而不是整个类型,增加了灵活性。

我们也可以这样实现上面的例子:

function itself<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);
    return arg;
}

三、泛型接口

在开发中,我们通常会封装 ajax 请求,统一处理后端响应,为了使得后端响应的数据在使用时能够具备TS智能提示,我们会指定返回值类型。一般来讲,后端返回数据的基本结构大致如下:

{
  code: 0, // 状态码
  data: null, // 响应数据
  msg: '' // 响应信息
}

其中,codemsg 的类型是明确的,但是 data 的类型却不尽相同,这个和你的具体业务有关,比如你请求商品列表和你执行登录返回的数据肯定是不一样的,所以这里我们就不能把 data 的类型定死, 这个时候我们就可以使用泛型了,data 的类型在执行具体业务时指定。比如:

/** 泛型接口:定义后端返回结构的大致形状,其中data的类型指定为泛型T */
interface BaseResponse<T> {
  code: number;
  data: T
  msg: string;
};

/** 登录接口所需要的参数类型 */
interface LoginParams {
  account: string;
  password: string;
}

/** 登录接口响应的数据类型 */
interface LoginResult {
  token: string;
}

/** 调用登录接口 */
function login(data: LoginParams) {
  return axios.post<BaseResponse<LoginResult>>('/api/login', data);
}

上述示例中,定义了一个泛型接口 BaseResponse 用于描绘后端返回数据的基本形状。然后封装了一个 login 函数,该函数接收一个 类型为 LoginParams 的参数 data,在函数内部,我们通过 axios.post 和后端通信,执行登录请求。可以看到,axios.post 接收一个泛型用于指定响应类型,这里我们传入 BaseResponse 并且指定 data 的类型为 LoginResult,所以此时,BaseResponse 的类型就会被解析成:

{
  code: number;
  data: {
    token: string;
  }
  msg: string;
}

四、泛型类

与泛型接口类似,泛型也可以用于类的类型定义中:

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

let myGenericNumber = new GenericNumber<number, string>();
myGenericNumber.zeroValue = 0;
myGenericNumber.message = "Hello";
myGenericNumber.add = function(x, y) { return x + y; };

五、泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用

function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

六、泛型约束

你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。在 itself 例子中,我们想访问 arglength 属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

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

相比于操作 any 所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于 T 的约束要求。

为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

interface Lengthwise {
  length: number;
}

function itself<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); 
  return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

itself(1); // Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.

我们需要传入符合约束类型的值,必须包含必须的属性:

itself({length: 1, value: [1]});