大家好,我是半虹,这篇文章来讲 TypeScript 中的泛型
1、概述
有些时候,定义函数或者类时,可能无法事先确定其中参数的类型
举个例子,假设现在有个函数,这个函数接收一个值再返回这个值
实际上这个值可以是任意类型,我们无法事先确定,因此只好设为 any
function identity(value:any):any {return value;
}
但是,这种写法无法反映参数和返回类型间的关系,理论上二者是相同的
为此,这个时候就要用到泛型!
所谓的泛型其实就是参数化的类型,具体有两特点:
- 定义的时候声明参数表示类型(定义时声明),具体是在
<>
内声明 - 使用的时候参数绑定实际类型(使用时绑定),同样是在
<>
内绑定
// 定义
// 声明泛型参数,这里参数声明为 T (参数名称可任意指定)
function identity<T>(value:T):T {return value;
}// 使用
// 绑定实际类型,这里分别绑定为 number 和 string
identity<number>(123); // 相当于:function identity(value:number):number { ... }
identity<string>('0'); // 相当于:function identity(value:string):string { ... }
定义泛型时就像声明一个占位类型,而在使用时会把占位类型替换成实际类型
使用泛型有助于提高代码的重用性,允许不确定类型时,依然可以有类型约束
2、基本语法
泛型可以应用于不同的场景,这些场景具体包括:函数、类、接口、类型别名
不同的场景中,都遵循一个规则:定义时声明泛型参数,使用时绑定泛型参数
定义时,在 <>
内声明泛型参数,如果存在有多个参数,那么需要用逗号隔开
使用时,在 <>
内绑定泛型参数,如果存在有多个参数,那么则要按顺序指定
但是不同的场景定义时声明参数的位置以及参数的作用范围也不同,具体如下:
(1)泛型函数
我们知道,定义函数有两种方式,一是函数声明,二是函数表达式
- 如果是函数声明, 泛型参数声明写在函数名称之后
- 如果是函数表达式,泛型参数声明写在类型签名之中,而类型签名有两种
- 如果是简写版类型签名,泛型参数声明写在整条声明前面
- 如果是完整版类型签名,泛型参数声明写在每条声明前面
声明的类型参数可以在函数之内的任意位置引用
无论哪一种定义方式,使用函数时泛型参数绑定都在函数名称之后
// 定义函数// 函数声明
// 格式如下:
// function func_name<gene_name1, ...>(prop1:type1, ...):return_type { ... }
// 例子如下:
function reverseArray0<E>(items: E[]): E[] {return items.reverse();
}// 函数表达式(简写版类型签名)
// 格式如下:
// <gene_name1, ...>(prop1:type1, ...) => return_type
// 例子如下:
let reverseArray1: <E>(items: E[]) => E[];
reverseArray1 = reverseArray0;// 函数表达式(完整版类型签名)
// 格式如下:
// {
// <gene_name1, ...>(prop1:type1, ...): return_type;
// ...
// };
// 例子如下:
let reverseArray2: { <E>(items: E[]): E[]; }
reverseArray2 = reverseArray0;// 使用函数// 调用函数
reverseArray0<number>([123, 456, 789]); // 绑定泛型参数为:number
reverseArray0<string>(['1', '4', '7']); // 绑定泛型参数为:string
reverseArray1<number>([234, 567, 891]);
reverseArray1<string>(['2', '5', '8']);
reverseArray2<number>([345, 678, 912]);
reverseArray2<string>(['3', '6', '9']);
(2)泛型类
而对于类来说,声明和绑定泛型参数都写在类名之后
声明泛型参数后可以在整个类中引用,除了静态成员,因为泛型类是描述类的实例
绑定泛型参数则发生在使用类的时候,具体包括实例化对象以及类的继承等的操作
类静态成员在类加载时就已经存在,且不依赖类的实例化过程
而泛型参数在类使用时才具体确定,因此无法被静态成员引用
// 定义类class Container<T> {// 普通属性value: T;// 构造函数constructor(value: T) {this.value = value;}// 普通方法setValue(value: T) {this.value = value;}getValue(): T {return this.value;}convert<U>(f:(input:T) => U): U { // 类中方法也可声明泛型参数,此处声明的参数只能在该方法内引用return f(this.value);}
}// 使用类// 实例化对象
let container1 = new Container<number>(123); // 绑定泛型参数为:number
let container2 = new Container<string>('0'); // 绑定泛型参数为:string
container1.setValue(456);
container2.setValue('1');
container1.convert<string>(num => num.toString()); // 调用方法时,绑定泛型参数为:string
container2.convert<string>(str => str.toString()); // 调用方法时,绑定泛型参数为:string// 继承类
class Extended1 extends Container<number> { // 绑定泛型参数为:number (实际类型)compare(input:number):boolean {return this.value === input;}
}
class Extended2<NT> extends Container<NT> { // 绑定泛型参数为:NT (泛型参数)compare(input:NT):boolean {return this.value === input;}
}
let container3 = new Extended1(123); // 无需绑定
let container4 = new Extended2<string>('0'); // 绑定泛型参数为:string
container3.setValue(456);
container4.setValue('1');
container3.convert<string>(num => num.toString()); // 调用方法时,绑定泛型参数为:string
container4.convert<string>(str => str.toString()); // 调用方法时,绑定泛型参数为:string
container3.compare(1234);
container4.compare('00');
(3)泛型接口
接口可以用于描述对象的属性和方法,此时声明和绑定泛型参数都写在接口名后
使用接口通常包括以下的场景:接口作为类型、类实现接口、接口继承接口等等
// 定义接口interface SimpleGuest<T> { // 访客接口id: T;name: string;
}interface SimpleCache<K, V> { // 缓存接口set(key: K, val: V): void;get(key: K): V | undefined;
}// 使用接口// 作为类型
let guest1:SimpleGuest<number> = { id: 123, name: '1' };
let guest2:SimpleGuest<number> = { id: 456, name: '4' };
let guest3:SimpleGuest<number> = { id: 789, name: '7' };// 实现接口
class GuestCache implements SimpleCache<number, string> {private keys: number[] = [];private vals: string[] = [];// 实现 setset(key: number, val: string): void {const index = this.keys.indexOf(key);if (index !== -1) {this.vals[index] = val;} else {this.keys.push(key);this.vals.push(val);}}// 实现 getget(key: number): string | undefined {const index = this.keys.indexOf(key);if (index !== -1) {return this.vals[index];} else {return undefined;}}
}
let cache0 = new GuestCache();
cache0.set(guest1.id, guest1.name);
cache0.set(guest2.id, guest2.name);
cache0.get(guest2.id); // 4
cache0.get(guest3.id); // undefined// 继承接口
interface DetailCache<K, V> extends SimpleCache<K, V> {remove(key: K): void;clear(): void;size(): number;
}
(4)泛型类型别名
类型别名和接口十分相似,同样也是:声明和绑定泛型参数都写在类型别名之后
类型别名和接口不同的是,类型别名除了可以定义对象类型,还能定义任意类型
// 定义类型别名type Result<T> = T | Error; // 非对象类型type TreeNode<T> = { // 对象类型value: T;children: TreeNode<T>[];
}// 使用类型别名function handleResult<T>(result: Result<T>): void {if (result instanceof Error) {// ...} else {// ...}
}
handleResult<number>(123);
handleResult<string>(new Error('something went wrong'));let node: TreeNode<number> = {value: 1,children: [{value: 2,children: [],}, {value: 3,children: [],}]
}
3、泛型推导
上面的例子中,我们在使用泛型参数时都是显式绑定实际类型
实际上对于函数和类来说,编译器也能根据传入参数自动推导
// 定义泛型函数
function identity<T>(value: T): T {return value;
}
function reverseArray<T>(items: T[]): T[] {return items.reverse();
}// 定义泛型类
class Container<T> {public value: T;constructor(value: T) {this.value = value;}
}// 使用泛型函数
identity(123); // 此时自动推导 T 为 123
identity('0'); // 此时自动推导 T 为 '0'
reverseArray([123, 456, 789]); // 此时自动推导 T 为 number
reverseArray(['1', '4', '7']); // 此时自动推导 T 为 stirng// 使用泛型类
let container1 = new Container(123); // 此时自动推导 T 为 number
let container2 = new Container('0'); // 此时自动推导 T 为 string
但是有些时候,编译器可能无法推导出正确的类型,这时就要显式绑定
function combineArray<T>(arr1: T[], arr2: T[]): T[] {return [...arr1, ...arr2];
}
combineArray([123, 456, 789], ['1', '4', '7']); // 隐式推导,编译错误
combineArray<number|string>([123, 456, 789], ['1', '4', '7']); // 显式绑定,编译正常
另外需要注意,所有的泛型参数要么都有显式绑定,要么都不显式绑定
function combineArray<U, V>(arr1: U[], arr2: V[]): (U | V)[] {return [...arr1, ...arr2];
}
combineArray([123, 456, 789], ['1', '4', '7']); // 都不绑定,编译正常
combineArray<number, string>([123, 456, 789], ['1', '4', '7']); // 都有绑定,编译正常
combineArray<number>([123, 456, 789], ['1', '4', '7']); // 部分绑定,编译错误
4、泛型约束
有些时候,仅声明泛型参数可能还不够,我们还希望表达这个泛型参数满足某种约束
举个例子,有一个打印数组元素的函数,操作是遍历数组然后调用元素 print
方法
// 遍历数组,调用元素中的 print 方法进行打印function printArray<T>(values:T[]): void {values.forEach(item => item.print()); // 编译错误
}
但是上述写法编译时会报错,因为无法保证数组元素具有 print
方法
这个时候就要用到泛型约束,确保泛型参数满足某种约束
具体可在泛型参数后加 extends
关键字,并在其后写明具体约束条件
例如 T extends U
可理解为 T
是 U
的子类型,T
至少应该为 U
// 通过 T extends { print() : void } 确保 T 至少应该具有 print 方法function printArray<T extends { print() : void }>(values:T[]): void {values.forEach(item => item.print()); // 编译正常
}
如果存在多个泛型参数,那么一个参数可以引用其它参数作为约束条件
但是这里需要注意一点,就是这些参数之间不能循环引用,否则会报错
// 编译正常,引用其它参数
function getProperty<O, K extends keyof O>(obj: O, key: K): O[K] { // 获取对象属性的常用泛型函数return obj[key];
}// 编译错误,存在循环引用
function test1<U extends U>() {}
function test2<U extends V, V extends U>() {}
5、默认类型
就像函数参数指定默认值一样,声明泛型参数的时候也能指定默认类型
带有默认类型的泛型参数必须在没有默认类型的泛型参数之后
function makePair1<U, V = string>(first: U, second: V): [U, V] { // 编译正常return [first, second];
}function makePair2<U = number, V>(first: U, second: V): [U, V] { // 编译错误return [first, second];
}
带有默认类型的泛型参数使用时遵循以下规则:
- 如有显式指定的参数,此时:
- 已被指定的参数,就要用指定的类型覆盖默认的类型
- 没被指定的参数,就要用默认的类型
- 如无显式指定的参数,此时:
- 如果能隐式推导,则会用推导的类型覆盖默认的类型
- 如不能隐式推导,则会用默认的类型
function makePair0<U = number, V = string>(first: U, second: V): [U, V] {return [first, second];
}// 如有显式指定的参数:// 第一个参数已被指定,使用指定的类型覆盖默认的类型,即 string
// 第二个参数已被指定,使用指定的类型覆盖默认的类型,即 number
makePair0<string, number>('0', 123);// 第一个参数已被指定,使用指定的类型覆盖默认的类型,即 string
// 第二个参数没被指定,使用默认的类型,即 string【注意】
makePair0<string>('0', '0');// 如无显式指定的参数:// 此时可以做隐式推导,使用推导的类型覆盖默认的类型
makePair0('0', 123); // 第一个参数为 number,第二个参数为 string
makePair0('0', '0'); // 第一个参数为 string,第二个参数为 string
好啦,本文到此结束,感谢您的阅读!
如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议
如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)