TS用了一段时间了,我目前对它的印象就是:加了类型的JS。那么我认为比较重要的、一定要好好学的一个特性就是范型,它能够让类型声明更加强大和通用,学好范型能够让TS编译器做更好的类型检查,提升我们写代码的效率和质量。官网文档相应内容点这里,(随缘翻译,不会一字一句翻,按自己理解来)。
软件工程中一个重要的部分就是构造通用的组件,它们有定义完备和一致的API,并且有很强的可复用性。具备兼容性和可扩展性的组件是构建大型软件系统中不可或缺的部分,能为系统提供灵活性。
在C#和Java当中,构造通用组件的主要工具就是范型,范型就是能够在一类特定规则的类型(而非某个具体的类型)上工作良好,使得用户可以使用自己的特定类型去使用通用组件。
什么是范型?
我们先从一个identify函数开始,这个函数简单地返回它接受到的东西,像一个echo命令:
function identity(arg: number): number {
return arg;
}
上述代码表明identify函数接收一个number类型的参数,并且返回一个number类型的结果;
我们也可以使用any类型:
function identity(arg: any): any {
return arg;
}
使用any就是一种范型,any做类型声明使得这个函数可以接受任何类型的参数,并且可能返回任意类型的结果。比如说传入一个number类型,identify也可能返回任意类型,比如string之类的。
然而这不是我们想要的,我们失去了对函数返回值的掌控。需要一种方式来指明函数入参和出参的关系;下面可以使用一个类型变量,一种工作在类型而不是具体值上的特殊变量:
function identity<Type>(arg: Type): Type {
return arg;
}
上面代码中我们添加了一个类型变量Type
,它指明了函数入参和返回值是同一种类型,我们还可以在函数体中使用这个类型。这就是范型,它工作在一系列类型上。和之前直接使用any来表示函数能接收任何类型不一样。
比如对于第一个函数,我们只需要传入Type = number
就行了,注意类型变量的值要包裹在一对尖括号中<>
:
let output = identity<number>(3);
这时我们就能确保output也是一个number类型的值了。这是范型的第一种调用方式;第二种直接让编译器做类型推断,这也是比较常用的使用方式(对于项目中比较复杂的业务类型,建议都进行显式声明):
let output = identity(3);
因为函数返回值output和入参3类型一样,所以即使不传递范型,我们也知道output的类型是number。
对于上面identity这样的泛型函数来说,编译器会强制我们在函数体中将这些参数视为任何类型,这个意思是对于identity来说,如果我们想使用具体类型的一些api,比如string类型的.length,那么编译器会报错:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length); // ts-error: Property 'length' does not exist on type 'Type'.
return arg;
}
这是因为我们没有对范型做任何约束,这样的Type本身就可以是任何类型,如果用户传入number,而number没有.length属性,那么就会触发错误,因此TS提示我们这段代码有风险,这也是TS为啥好用的原因,减少我们出bug的机率。
我们可以把入参和返回值的类型都改成任何类型的数组Type[]
,这下再使用.length就没问题了。
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
// or
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
基本认识有了,下面开始构造我们自己的范型。
本节我们探索函数类型本身,以及如何构造范型类型,或者说范型接口。
范型函数的类型和无范型的函数差不多,和函数的声明很相似,先列出其入参和出参的类型,入参类型 => 出参类型:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;
在类型声明中,范型的名称可以任意取,只要声明中保持一致就行:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Input>(arg: Input) => Input = identity;
也可以和对象一样包裹在一对花括号中来写范型,**{ 入参类型 : 出参类型 } **:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;
我们可以把类型声明提取出来:
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
接着把范型定义提取到整个接口最外面,使其对内部所有类型可见,GenericIdentityFn就是一个范型类型了,例如下面我们将其变量声明为number,那么所有用到Type的地方就都确定下来了:
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
注意现在情况有点不一样了,不同于上一节描述了一个范型函数,现在是一个使用部分泛型类型的非泛型函数签名。在使用GenericIdentityFn时,需要指定其具体的类型参数(比如这里的number),从而把具体的类型传递到下游函数中去。
理解清楚什么时候将类型参数直接放在调用签名上,何时将它放在类型本身上,有助于描述类型的哪些方面是泛型的。(<>加在哪,范型就在哪)
除了范型接口外,我们还可以定义范型类。和接口类型,类的范型也是用一对尖括号<>
包起来,放在类名后。
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
同样的,在类名后声明的类型变量,整个类内部都可以使用这种类型。另外,一个类有静态属性和实例属性,范型只能在实例属性中使用(原型链上的静态属性属于这个类的上游了,范型,也就是类型变量,只能往下游传递)。
还记得第2节的例子吗,当时我们在函数中使用了.length
属性,ts提示报错了,因为我们对函数声明的范型没有加任何约束,当传递不含长度属性的类型时会导致错误。
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length); // ts-error: Property 'length' does not exist on type 'Type'.
return arg;
}
那么能否给范型增加约束,使传递的类型必须满足拥有.length
这个属性呢?我们首先声明一个接口表明范型必须满足的约束,然后在书写范型的时候用extends
关键词给范型增加约束:
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
此时因为传递进来的类型变量Type
必须继承自Lengthwise(也就是拥有length属性),这是函数中就可以直接使用该属性了。
同样因为我们增加对范型的约束条件,这个函数能够接受的类型就不再是任何类型了,而是Array, string等满足Lengthwise的类型才行。
另外,我们声明了函数参数也是继承自Lengthwise的,那么给函数传值的时候也必须传Lengthwise声明的必传参:
loggingIdentity({ length: 10, value: 3 });