泛型
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:
- 类的实例成员
- 类的方法
- 函数参数
- 函数返回值
动机和示例
下面是对一个先进先出的数据结构——队列,在 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 {
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
你可以随意调用泛型参数,当你使用简单的泛型时,泛型常用 T
、U
、V
表示。如果在你的参数里,不止拥有一个泛型,你应该使用一个更语义化名称,如 TKey
和 TValue
(通常情况下,以 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>();
}