这是我参与「第四届青训营 」笔记创作活动的第12天
「前言」
相较于 js,由于类型的存在,ts 中相似的逻辑因为类型的不同,使得代码复用性降低,针对于这一现象,ts 使用 泛型类型 使得相同的一段代码可以支持多种数据类型
「泛型类型」
泛型类型可以作为一种变量类型
function Test<T, K>(arg1: T, arg2: K): void {
let arg3: T;
}
Test<number, string>(1, 'hello');
- 在这个例子中我们在
<>中定义了两种暂时还不知道的类型,我们可以在函数内部使用这种类型变量,例如给参数添加类型注解,给返回值添加类型,甚至还可以在内部声明具有泛型类型的变量。 - 我们在调用函数的时候可以指定泛型类型,从而使泛型类型具有更具体的类型
我们还可以使用类型推断简化调用的步骤
function Test<T, K>(arg1: T, arg2: K): void {
let arg3: T;
}
Test(1, 'hello');
还可以使用泛型作为全新的类型添加给变量
const test: <T>(arg: T) => T = (arg) => arg
const test: { <T>(arg: T): T } = (arg) => arg
可以使用 {} 包裹类型声明,将类型变得一体化
但是在定义泛型的时候,具有泛型类型的数据缺少了更细致的类型判断,因此丧失了诸多能力
function Test<T>(num1: T, num2: T) {
num1 + num2; // error,运算符“+”不能应用于类型“T”和“T”
num1.length; // error,类型“T”上不存在属性“length”。
}
后续会讲解各种解决方案
「泛型接口」
泛型除了可以运用在函数中,还可以在接口中使用
泛型接口声明函数
interface IFn {
<T>(arg: T): T;
}
const fn: IFn = (arg) => arg;
接口使用泛型参数
在接口中还可以将泛型作为参数传递给接口
使用泛型参数重写上面的例子
interface IFn<T> {
(arg: T): T;
}
const fn: IFn<number> = (arg) => arg;
在使用泛型参数声明函数的时候,必须传递给接口一个确切的类型,使得接口可以获得一个明确的类型
「泛型类」
泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。
class Test<T> {
id: T;
show: (arg: T) => T;
}
new Test()
new Test<number>();
因此,泛型类可以创建多种不同类型的实例
注意:静态成员不能使用泛型类的泛型类型
「泛型约束」
虽然泛型可以复用代码,但是使用泛型类型的变量缺少了诸多操作方法
function getStrLen<T>(arg: T): number {
return arg.length; // error,类型“T”上不存在属性“length”
}
由于 <T> 这个泛型类型上缺少了 .length 属性,所以我们得想办法给 <T> 添加上 .length 属性,我们可以使用 泛型约束 给泛型添加更多的约束条件,使泛型类型具有确切的类型,从而拥有对应的属性
interface IT {
length: number;
}
function getStrLen<T extends IT>(arg: T): number {
return arg.length;
}
创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束
一个更具体的例子:
interface IT {
length: number;
method: () => void;
}
function getStrLen<T extends IT>(arg: T): number {
arg.method();
return arg.length;
}
interface IObj {
length: number;
method: () => void;
}
getStrLen<IObj>({
length: 3,
method() { }
});
在泛型约束中使用类型参数
很多时候,多个泛型参数之间存在依赖关系
比如,现在我们想要用属性名从对象里获取这个属性。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m"); // error,类型“"m"”的参数不能赋给类型“"a" | "b" | "c" | "d"”的参数。
这里的 keyof 关键字表示 <K> 是 <T> 中的一种索引
我们使用 extends 约束泛型 K,K 必须是 T 中的键名
「在泛型里使用类类型」
在 ts 中 类名也可以作为参数传递
class Test { }
function createInstance(className: typeof Test): Test {
return new className();
}
createInstance(Test);
在这个例子中我们使用 typeof Test 约束 className 的类型,这个函数等效于 new Test()
我们还可以使用其他的方式传递类名
class Test { }
function createInstance(className: { new(): Test }): Test {
return new className();
}
createInstance(Test);
下面的方式与之前的两种等效
function createInstance(className: new () => Test): Test {
return new className();
}
需求:使用上述方式构建出一个函数可以创建出任意实例
这个需求,相当上面的例子,复用性大大提升,这里肯定是使用泛型类型约束函数参数
function createInstance<T>(className: new () => T): T {
return new className();
}
注意:这里不能使用 typeof 关键字获取泛型的类型,因为 typeof 关键字只能判断一个明确值的类型,不能判断类型(泛型类型也是一种类型)
一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。
class Animal {
numLegs: number;
}
class Lion extends Animal { }
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion);
class Test { numLegs: number; }
createInstance(Test)
泛型 A 类型被 Animal 类型约束,所以必须传递符合约束规则的类
「泛型与unknown的联系」
function add(num: unknown) {
return num + 0; // error,运算符“+”不能应用于类型“unknown”和“0”。
}
function add1<T>(num: T) {
return num + 0; // error,运算符“+”不能应用于类型“T”和“number”。
}
通过这个例子可以看出 泛型 和 unknown 十分相似
泛型的解决办法
- 不能使用类型断言
function test<T>(arg: T) {
return (arg as string) + 0; // error,类型 "T" 到类型 "string" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"
}
- 只能使用更细致的类型判断
function test<T>(arg: T) {
if (typeof arg === 'number') {
return arg + 0;
}
}
- 如果想要获取泛型类型变量的属性,还可以使用类型约束
unknown 的解决办法
- 可以使用类型断言
- 也可以使用更细致的类型判断
「参考文章」
泛型的使用绝不止于这篇文章的介绍,关于泛型的高级使用可以参考:你不知道的 TypeScript 泛型(万字长文,建议收藏) - 知乎 (zhihu.com)