关注公众号
关注公众号
与我交流
与我交流
购买此书
购买此书

泛型

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

  • 类的实例成员
  • 类的方法
  • 函数参数
  • 函数返回值

动机和示例

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

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 {
  private data = [];
  push = (item: number) => this.data.push(item);
  pop = (): number => this.data.shift();
}

const queue = new QueueNumber();

queue.push(0);
queue.push('1'); // Error: 不能推入一个 `string` 类型,只能是 `number` 类型

// 如果该错误得到修复,其他将不会出现问题

当然,快速也意味着痛苦。例如当你想创建一个字符串的队列时,你将不得不再次修改相当大的代码。我们真正想要的一种方式是无论什么类型被推入队列,被推出的类型都与推入类型一样。当你使用泛型时,这会很容易:

// 创建一个泛型类
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 类型被允许

另外一个我们见过的例子:一个 reverse 函数,现在在这个函数里提供了函数参数与函数返回值的约束:

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

在此章节中,你已经了解在函数上使用泛型的例子。一个值得补充一点的是,你可以为创建的成员函数添加泛型:

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

TIP

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

误用的泛型

我见过开发者使用泛型仅仅是为了它的 hack。当你使用它时,你应该问问自己:你想用它来提供什么样的约束。如果你不能很好的回答它,你可能会误用泛型,如:

declare function foo<T>(arg: T): void;

在这里,泛型完全没有必要使用,因为它仅用于单个参数的位置,使用如下方式可能更好:

declare function foo(arg: any): void;

设计模式:方便通用

考虑如下函数:

declare function parse<T>(name: string): T;

在这种情况下,泛型 T 只在一个地方被使用了,它并没有在成员之间提供约束 T。这相当于一个如下的类型断言:

declare function parse(name: string): any;

const something = parse('something') as TypeOfSomething;

仅使用一次的泛型并不比一个类型断言来的安全。它们都给你使用 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());
};

请注意,你仍然需要明显的注解任何你需要的类型,但是 getJSON<T> 的签名 config => Promise<T> 能够减少你一些关键的步骤(你不需要注解 loadUsers 的返回类型,因为它能够被推出来):

type LoadUserResponse = {
  user: {
    name: string;
    email: string;
  }[];
};

function loaderUser() {
  return getJSON<LoadUserResponse>({ url: 'https://example.com/users' });
}

与此类似:使用 Promise<T> 作为一个函数的返回值比一些如:Promise<any> 的备选方案要好很多。

配合 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>();
}