设计泛型的目的在于在成员之间提供有意义的约束,这些成员可以是:

  1. 类的实例成员
  2. 类的方法
  3. 函数参数
  4. 函数的返回值

动机和示例:

下面是对一个先进先出的数据结构——队列在TypeScript和JavaScript中的简单实现。

···

class Queue {
  private data = [];
  push = item => this.data.push(item);
  pop = () => this.data.shift();
}

···

在上述代码中的问题是你可以在代码中添加任何数据类型的数据,当然,当数据被弹出队列时,也可以是任意类型。在下面的示例中,看起来人们可以向队列中添加 string 类型的数据,但是实际上该用法嘉定的只有number类型会被添加到队列里面。

···

class Queue {
  private data = [];
  push = item => this.data.push(item);
  pop = () => this.data.shift();
}

const queue = new Queue();

queue.push(0);
queue.push('1'); // Oops,一个错误

// 一个使用者,走入了误区
console.log(queue.pop().toPrecision(1));
console.log(queue.pop().toPrecision(1)); // RUNTIME ERROR

···

解决方法之一就是创建不同类型的类

class QueueNumber{

}

如果又要创建一个字符串的队列时,又不得不修改代码,

class QueueString{

}

但是有了泛型的时候,一切就变得很容易了

// 创建一个泛型类
class Queue<T> {
  private data: T[] = [];
  push = (item: T) => this.data.push(item);
  pop = (): T | undefined => this.data.shift();
}

// 简单的使用
const queue = new Queue<number>();
queue.push(0);
queue.push('1'); // Error:不能推入一个 `string`,只有 number 类型被允许

不仅仅可以给参数进行约束,也可以对返回值进行约束

function reverse<T>(items: T[]): T[] {
  const toreturn = [];
  for (let i = items.length - 1; i >= 0; i--) {
    toreturn.push(items[i]);
  }
  return toreturn;
}

const sample = [1, 2, 3];
let reversed = reverse(sample);

reversed[0] = '1'; // Error
reversed = ['1', '2']; // Error

reversed[0] = 1; // ok
reversed = [1, 2]; // ok
TIP

你可以随意调用泛型参数,当你使用简单的泛型时,泛型常用 T、U、V 表示。如果在你的参数里,不止拥有一个泛型,你应该使用一个更语义化名称,如 TKey 和 TValue (通常情况下,以 T 作为泛型的前缀,在其他语言如 C++ 里,也被称为模板)

单个参数的函数没必要用泛型

declare function foo<T>(arg: T): void; // 不需要

declare function foo(arg: any): void; // 正确用法

仅使用一次的泛型并不比一个类型断言来的安全。它们都给你使用 API 提供了便利。

另一个明显的例子是,一个用于加载 json 返回值函数,它返回你任何传入类型的 Promise:

const getJSON = <T>(config: { url: string; headers?: { [key: string]: string } }): Promise<T> => {
  const fetchConfig = {
    method: 'GET',
    Accept: 'application/json',
    'Content-Type': 'application/json',
    ...(config.headers || {})
  };
  return fetch(config.url, fetchConfig).then<T>(response => response.json());
};

配合 axios 使用
通常情况下,我们会把后端返回数据格式单独放入一个 interface 里:

// 请求接口数据
export interface ResponseData<T = any> {
  /**
   * 状态码
   * @type { number }
      */
    code: number;

  /**
   * 数据
   * @type { T }
      */
    result: T;

  /**
   * 消息
   * @type { string }
      */
    message: string;
}

当我们把 API 单独抽离成单个模块时:

// 在 axios.ts 文件中对 axios 进行了处理,例如添加通用配置、拦截器等
import Ax from './axios';

import { ResponseData } from './interface.ts';

export function getUser<T>() {
  return Ax.get<ResponseData<T>>('/somepath')
    .then(res => res.data)
    .catch(err => console.error(err));
}

接着我们写入返回的数据类型 User,这可以让 TypeScript 顺利推断出我们想要的类型:

interface User {
  name: string;
  age: number;
}

async function test() {
  // user 被推断出为
  // {
  //  code: number,
  //  result: { name: string, age: number },
  //  message: string
  // }
  const user = await getUser<User>();
}