泛型

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

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

动机和示例

考虑如下简单的 Queue (先进先出)数据结构实现,一个在 TypeScriptJavaScript 中的简单实现:

  1. class Queue {
  2. private data = [];
  3. push = item => this.data.push(item);
  4. pop = () => this.data.shift;
  5. }

在上述代码中存在一个问题,它允许你推入任何类型至队列中,推出的时候也是任意类型,如下所示,但一个人推入一个 string 类型至队列中,但是使用者可能会认为队列里只有 number 类型:

  1. class Queue {
  2. private data = [];
  3. push = item => this.data.push(item);
  4. pop = () => this.data.shift;
  5. }
  6. const queue = new Queue();
  7. queue.push(0);
  8. queue.push('1'); // Oops,一个错误
  9. // 一个使用者,走入了误区
  10. console.log(queue.pop().toPrecision(1));
  11. console.log(queue.pop().toPrecision(1)); // RUNTIME ERROR

一个解决的办法(事实上,这也是不支持泛型类型的唯一解决办法)是为这些约束创建特殊类,如快速创建数字类型的队列:

  1. class QueueNumber {
  2. private data = [];
  3. push = (item: number) => this.data.push(item);
  4. pop = (): number => this.data.shift;
  5. }
  6. const queue = new QueueNumber();
  7. queue.push(0);
  8. queue.push('1'); // Error: 不能推入一个 `string` 类型,只能是 `number` 类型
  9. // 如果该错误得到修复,其他将不会出现问题

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

  1. // 创建一个泛型类
  2. class Queue<T> {
  3. private data = [];
  4. push = (item: T) => this.data.push(item);
  5. pop = (): T => this.data.shift();
  6. }
  7. // 简单的使用
  8. const queue = new Queue<number>();
  9. queue.push(0);
  10. queue.push('1'); // Error:不能推入一个 `string`,只有 number 类型被允许

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

  1. function reverse<T>(items: T[]): T[] {
  2. const toreturn = [];
  3. for (let i = items.length - 1; i >= 0; i--) {
  4. toreturn.push(items[i]);
  5. }
  6. return toreturn;
  7. }
  8. const sample = [1, 2, 3];
  9. const reversed = reverse(sample);
  10. reversed[0] = '1'; // Error
  11. reversed = ['1', '2']; // Error
  12. reversed[0] = 1; // ok
  13. reversed = [1, 2]; // ok

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

  1. class Utility {
  2. reverse<T>(items: T[]): T[] {
  3. const toreturn = [];
  4. for (let i = items.length; i >= 0; i--) {
  5. toreturn.push(items[i]);
  6. }
  7. return toreturn;
  8. }
  9. }

TIP

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

误用的泛型

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

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

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

  1. declare function foo(arg: any): void;

设计模式:方便通用

考虑如下函数:

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

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

  1. declare function parse(name: string): any;
  2. const something = parse('something') as TypeOfSomething;

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

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

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

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

  1. type LoadUserResponse = {
  2. user: {
  3. name: string;
  4. email: string;
  5. }[];
  6. };
  7. function loaderUser() {
  8. return getJSON<LoadUserResponse>({ url: 'https://example.com/users' });
  9. }

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

配合 axios 使用

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

  1. // 请求接口数据
  2. export interface ResponseData<T = any> {
  3. /**
  4. * 状态码
  5. * @type { number }
  6. */
  7. code: number;
  8. /**
  9. * 数据
  10. * @type { T }
  11. */
  12. result: T;
  13. /**
  14. * 消息
  15. * @type { string }
  16. */
  17. message: string;
  18. }

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

  1. // 在 axios.ts 文件中对 axios 进行了处理,例如添加通用配置、拦截器等
  2. import Ax from './axios';
  3. import { ResponseData } from './interface.ts';
  4. export function getUser<T>() {
  5. return Ax.get<ResponseData<T>>('/somepath')
  6. .then(res => res.data)
  7. .catch(err => console.error(err));
  8. }

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

  1. interface User {
  2. name: string;
  3. age: number;
  4. }
  5. async function test() {
  6. // user 被推断出为
  7. // {
  8. // code: number,
  9. // result: { name: string, age: number },
  10. // message: string
  11. // }
  12. const user = await getUser<User>();
  13. }

原文: https://jkchao.github.io/typescript-book-chinese/typings/generices.html