泛型是 TypeScript 中最难理解的概念之一,但是它也是 TypeScript 中最强大的特性之一。泛型提供了一种在定义函数、接口或类的时候,不预先指定具体类型的方式。
为什么需要泛型
如果用 js 来实现一个函数来 return 传入的参数,那么就是这样的:
function identity(arg) {
return arg;
}
那么在 ts 中,我们应该怎样写呢?
function identity(arg: number): number {
return arg;
}
但是这样写的话,我们就只能传入 number 类型的参数了,如果我们想传入 string 类型的参数呢?
用 any
类型来实现:
function identity(arg: any): any {
return arg;
}
但是这会导致我们丢失一些信息,比如传入的参数类型和返回的参数类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。例如:
let output :string = identity(123);
上面的例子中,我们传入了一个数字,但是却返回了一个字符串,这行代码不会报错,但是也不会得到我们想要的结果。
这时候,我们就可以使用泛型了。
function identity<T>(arg: T): T {
return arg;
}
这里的 T 是我们创建的泛型的名称,我们可以在函数体中使用这个泛型。实质上它是一个占位符,我们可以传入任何类型的参数,比如:
let output = identity('myString'); // type of output will be 'string'
这里还涉及了类型推论,因为我们传入了一个字符串,所以编译器会自动推断出我们的参数类型为 string,而不是 any。
如果我们想要的和传入的参数类型不一样:
function loggingIdentity<T>(arg: T): T {
return arg;
}
const res:string = loggingIdentity(123);
这里会报错,因为我们传入的参数类型是 number,但是我们想要的参数类型是 string。
下面是一个更复杂的例子:
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]
swap([true, 123]); // [123, true]
这里我们传入了一个元组,然后返回了一个元组,但是元组中的类型是反过来的。
泛型约束
有下面一个例子:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
这里会报错,因为我们传入的参数可能没有 length 属性,所以我们需要对传入的参数进行指定。
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
const res = loggingIdentity([1, 2, 3]);
但是我们可以进一步的优化,我们可以使用泛型约束来实现:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
const res = loggingIdentity([1, 2, 3]);
const res2 = loggingIdentity({length: 10, value: 3});
const res3 = loggingIdentity('123');
const res4 = loggingIdentity(123); // Error, number doesn't have a .length property
上面的代码表示传入的参数必须包含 length 属性。否则会报错。
类和接口中的泛型
类
我们同样可以在类和接口中使用泛型。我们要实现一个队列类,它有一个 push 方法和一个 pop 方法,我们可以使用数组来实现:
class Queue {
private data = [];
push(item) {
return this.data.push(item);
}
pop() {
return this.data.shift();
}
}
const queue = new Queue();
queue.push(1);
queue.push('str');
console.log(queue.pop().toFixed()); // 1 ,toFix()方法是number类型的方法
console.log(queue.pop().toFixed()); // Error, toFix()方法是number类型的方法,但是这里是string类型
在上面的例子中,我们可以看到,我们可以向队列中添加任何类型的数据,但是当我们调用 pop 方法的时候,我们只能得到 any 类型的数据,这样就会导致我们调用 any 类型的方法的时候会报错。我们希望在任何时候,我们推入和弹出的数据类型是一致的。
class Queue<T> {
private data = [];
push(item: T) {
return this.data.push(item);
}
pop(): T {
return this.data.shift();
}
}
const queue = new Queue<number>();
queue.push(1);
queue.push('str'); // Error, 'str' is not assignable to parameter of type 'number'
console.log(queue.pop().toFixed()); // 1 ,toFix()方法是number类型的方法
const queue2 = new Queue<string>();
queue2.push('str');
queue2.push(1); // Error, 1 is not assignable to parameter of type 'string'
console.log(queue2.pop().length); // 3 ,length是string类型的方法
接口
我们可以使用接口来定义一个类的结构,我们可以使用泛型来定义一个接口的结构。
interface KeyPair<T, U> {
key: T;
value: U;
}
let kp1: KeyPair<number, string> = { key: 123, value: 'str' }; // OK
let kp2: KeyPair<string, number> = { key: 'str', value: 123 }; // OK
let kp3: KeyPair<number, string> = { key: 123, value: 123 }; // Error, 123 is not assignable to type
前面提到数组初始化的时候,我们可以使用泛型来定义数组的类型,我们也可以使用泛型来定义数组的结构。
let arr: Array<number> = [1, 2, 3];
let arr2: Array<string> = ['1', '2', '3'];
同样也可使用泛型来描述一个函数类型。
interface IPlus<T> {
(a: T, b: T): T;
}
function plus(a: number, b: number): number {
return a + b;
}
function connect(a: string, b: string): string {
return a + b;
}
const a: IPlus<number> = plus;
const b: IPlus<string> = connect;