本文通俗解释 TypeScript 泛型,将其视为类型系统中的参数。通过分析 TypeScript 编译过程,揭示泛型在编译时生成类型检查体系的机制。文章涵盖泛型函数、接口、类的用法,以及泛型约束的实现原理,帮助开发者深入理解类型编程与业务代码的关系。
概述
在 TypeScript 开发中,泛型常用于约束函数的类型。由于函数也包含在类的构造函数中,因此类的声明部分同样可以使用泛型。那么,泛型的本质是什么?如何更直观地理解它?
什么是泛型
泛型(Generics)是指在定义函数、接口或类时,不预先指定具体类型,而是在使用时再确定类型的一种特性。
通俗来说,泛型是类型系统中的“参数”,主要目的是实现类型的复用。从定义可以看出,它仅应用于函数、接口和类。虽然其意义与 JavaScript 中的函数参数相似,但两者处于不同层面。因为 TypeScript 是静态类型系统,在编译 JavaScript 时进行类型检查,所以这种“参数”实际上是在编译期的运行阶段发挥作用。之所以称为“参数”,是因为它具备与函数参数完全相同的特性。
function increse(param) {
// ...
}
在类型系统中,我们这样使用泛型:
function increase<T>(param: T): T {
//...
}
当 param 为一个类型时,T 被赋值为该类型,在返回值中,T 即为该类型,从而进行类型检查。
编译系统
TypeScript 自身的类型系统也需要进行编程,只不过其编程方式较为特殊:你需要在 TypeScript 代码中穿插 JavaScript 代码(这种说法可能有些反直觉,因为通常感觉是在 JavaScript 中夹杂了 TypeScript 代码)。
编程中最核心的形式是函数。在 TypeScript 的类型编程中,你看到了函数吗?没有。这是因为有泛型的地方就有函数,只是函数的形式被 JavaScript 代码割裂了。TypeScript 需要编译为最终产物。编译过程中涉及两件事:一是在内存中运行类型编程代码,形成类型检查体系。也就是说,TypeScript 编译器先运行 TypeScript 编程代码,得到一个运行时的检查系统,然后利用这个系统对穿插其中的 JavaScript 代码进行类型断言;二是输出 JavaScript 代码。在输出过程中,编译系统已经完成了类型编程代码的运行,就像 PHP 代码中 echo 输出 JavaScript 代码一样,PHP 代码已经运行,显示出来的是 JavaScript 代码。
从这个角度看 TypeScript,你或许更能理解为什么说它是 JavaScript 的超集,以及为什么它的编译结果是 JavaScript。
通俗的理解泛型
既然我们理解了 TypeScript 编译系统的逻辑,就可以将类型编程与 JavaScript 本身的业务编程在概念上区分开。我们所讲的“泛型”,仅存在于类型编程部分,这部分代码是 TypeScript 的编译运行时代码。
我们来看下一个简单的例子:
function increase<T>(param: T): T {
//...
}
这段代码,如果我们把 JavaScript 代码区分开,并用类型描述文本来表示会是怎样?
// 声明函数 @type,参数为 T,返回结果为 (T): T
@type = T => (T): T
// 运行函数得到一个类型 F,即类型为 (number): number
@F = @type(number)
// 要求 increase 这个函数符合 F 这种类型,也就是参数为 number,返回值也为 number
@@F
function increase(param) {
// ...
}
@@end
实际上并没有 @@F 这种语法,这是我编造出来的,目的是让你可以从另一个角度去理解类型系统。
当我们理解泛型是一种“参数”之后,我们可能会问:类型系统的函数在哪里?对于 JavaScript 函数而言,你可以很容易指出函数声明语句和参数,但是在 TypeScript 中,这个部分是隐藏起来的。不过,我们可以在一些特定结构中,比较容易看到类型函数的影子:
// 声明一个泛型接口,这个写法,像极了声明一个函数,我们用描述语言来形容 @type = T => (T): T
interface GenericIdentityFn<T> {
(arg: T): T;
}
// 这个写法,有点像一个闭包函数,在声明函数后,立即运行这个函数,描述语言:@@[T => (T): T](any)
function identity<T>(arg: T): T {
return arg;
}
// 使用泛型接口,像极了调用一个函数,我们用描述语言来形容 @type(number)
let myIdentity: GenericIdentityFn<number> = identity;
上面这一整段代码,我们用描述文本重写一遍:
@GenericIdentityFn = T => (T): T
@@[T => (T): T](any)
function identify(arg) {
return arg
}
@@end
@@GenericIdentityFn(number)
let myIdentity = identity
@@end
我们在类型系统中声明了两个函数,分别是 @GenericIdentityFn 和 @some(匿名函数 @[T => (T): T])。虽然涉及两个函数,但实际上它们是完全相同的,因为 TypeScript 是结构类型系统,即在类型检查时只判断结构上的每个节点类型是否相同,而不是必须保持类型变量本身的指针相同。@GenericIdentityFn 和 @some 这两个函数分别被调用,用来修饰 identify 和 myIdentify,在调用时,接收的参数不同,导致最终的类型检查规则也不同。identify 只要保证参数和返回值的类型相同,至于具体什么类型,可以是 any。而 myIdentify 除了保证参数和返回值类型相同外,还要求类型必须是 number。
泛型类
除了泛型接口,class 类也可以泛型化,即“泛型类”。借助泛型类,我们来探究一下泛型的声明和使用步骤。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
前文泛型接口因为只是为了约束函数的类型,所以写得像函数。实际上,我们可以用描述语言重新描述一个泛型接口和泛型类。上面的红色部分,我们用描述语言来描述:
@GenericNumber = T => class {
zeroValue: T;
add: (x: T, y: T) => T;
}
@GenericNumber 这个函数,以 T 为参数,返回一个 class,在 @type 函数体内多次用到了参数 T。
@GenericIdentityFn = T => interface {
(arg: T): T;
}
我们重新描述了前面的 interface GenericIdentityFn,这样我们就可以在接口中增加其他的方法。
可以注意到,即使 TypeScript 内置的基础类型,例如 Array,被声明为泛型接口、泛型类之后,这些接口和类在使用时必须通过 <> 传入参数。本质上,因为它们都是函数,只是返回值不同。
其他泛型使用的通俗解释
接下来我们要再描述一个复杂的类型:
class Animal {
numLegs: number;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
我们姑且不去看 new() 的部分,我们看尖括号中的 extends 语法,这里应该怎么理解呢?实际上,我们面对的问题是,在编译时,<A extends Animal> 尖括号中的内容是什么时候运行的,是之前,还是之间?
// 到底是
@type = (A extends Animal) => (new() => A): A
@type(T)
// 还是
@type = A => (new() => A): A
@type(T extends Animal)复
因为 TypeScript 是静态类型系统,Animal 是不变的类,因此,可以推测其实在类的创建之前,尖括号的内容已经被运行了。
@type = (A extends Animal) => (new() => A): A
也就是说,要使用 @type(T) 产生类型,首先 T 要满足 Animal 的结构,然后才能得到需要的类型。如果 T 已经不满足 Animal 类的结构了,那么编译器会直接报错,而这个报错,不是类型检查阶段,而是在类型系统的创建阶段,也就是 TypeScript 代码的运行阶段。这种情况被称为“泛型约束”。
另外,类似 <A,B> 这样的语法其实和函数参数一致。
@type = (A, B) => (A|B): SomeType
我们再来看 TypeScript 内置的基础类型:Array<number>
@Array = any => any[]
结语
TypeScript 中的泛型,实际上就是类型的生成函数的参数。本文的内容全部为凭空想象,仅适用于对 TypeScript 进行理解时的思路开拓,不适用于真实编程,特此声明。
以上就是如何通俗的解释TypeScript 泛型的详细内容,更多关于TypeScript泛型的资料请关注本站其它相关文章!