软件工程的一个重要部分是构建不仅具有定义明确且一致的 API,而且可重用的组件。能够处理今天和明天数据的组件将为您构建大型软件系统提供最灵活的功能。
在 C# 和 Java 等语言中,用于创建可重用组件的工具箱中的主要工具之一是泛型,即能够创建一个可以在各种类型而不是单个类型上工作的组件。这允许用户使用这些组件并使用他们自己的类型。
泛型入门
首先,让我们来做泛型的“hello world”:身份函数。身份函数是一个返回传入值的函数。您可以将其视为与echo
命令类似。
在没有泛型的情况下,我们要么必须为恒等函数指定一个特定的类型
tsTry
functionidentity (arg : number): number {returnarg ;}
或者,我们可以使用any
类型来描述恒等函数
tsTry
functionidentity (arg : any): any {returnarg ;}
虽然使用any
在某种程度上是通用的,因为它会使函数接受arg
类型的任何类型,但实际上我们丢失了关于函数返回值类型的相关信息。如果我们传入一个数字,我们唯一知道的信息是任何类型都可以作为返回值。
相反,我们需要一种方法来捕获参数的类型,以便我们也可以使用它来表示返回值的类型。在这里,我们将使用一个类型变量,这是一种特殊类型的变量,它作用于类型而不是值。
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}
我们现在在恒等函数中添加了一个类型变量T
。这个T
允许我们捕获用户提供的类型(例如number
),以便我们可以在以后使用这些信息。在这里,我们再次使用T
作为返回值类型。经过检查,我们可以看到参数和返回值类型使用的是相同的类型。这使我们能够将类型信息从函数的一侧传递到另一侧。
我们说这个版本的identity
函数是泛型的,因为它适用于一系列类型。与使用any
不同,它也与第一个使用数字作为参数和返回值类型的identity
函数一样精确(即它不会丢失任何信息)。
一旦我们编写了泛型恒等函数,我们就可以通过两种方式调用它。第一种方式是将所有参数(包括类型参数)传递给函数
tsTry
letoutput =identity <string>("myString");
在这里,我们明确地将T
设置为string
作为函数调用的参数之一,使用<>
而不是()
来表示参数。
第二种方式也许是最常见的。在这里,我们使用类型参数推断——也就是说,我们希望编译器根据我们传入的参数类型自动为我们设置T
的值
tsTry
letoutput =identity ("myString");
请注意,我们不需要在尖括号(<>
)中显式传递类型;编译器只是查看了值"myString"
,并将T
设置为它的类型。虽然类型参数推断可以帮助我们简化代码并提高可读性,但在编译器无法推断类型时,你可能需要像之前示例中那样显式传递类型参数,这种情况可能发生在更复杂的示例中。
使用泛型类型变量
当你开始使用泛型时,你会注意到,当你创建像 `identity` 这样的泛型函数时,编译器会强制你在函数体中正确地使用任何泛型类型参数。也就是说,你实际上要将这些参数视为可以是任何类型。
让我们从之前的 `identity` 函数开始
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}
如果我们想在每次调用时将参数 `arg` 的长度记录到控制台,该怎么办?我们可能会想这样写
tsTry
functionloggingIdentity <T >(arg :T ):T {Property 'length' does not exist on type 'T'.2339Property 'length' does not exist on type 'T'.console .log (arg .); length returnarg ;}
当我们这样做时,编译器会给我们一个错误,说我们正在使用 `arg` 的 `.length` 成员,但我们并没有在任何地方说明 `arg` 有这个成员。请记住,我们之前说过这些类型变量代表任何类型,因此使用此函数的人可能会传入一个 `number`,而 `number` 没有 `.length` 成员。
假设我们实际上希望此函数作用于 `T` 的数组,而不是直接作用于 `T`。由于我们正在使用数组,因此 `.length` 成员应该可用。我们可以像创建其他类型的数组一样描述它
tsTry
functionloggingIdentity <T >(arg :T []):T [] {console .log (arg .length );returnarg ;}
你可以将 `loggingIdentity` 的类型解读为“泛型函数 `loggingIdentity` 接受一个类型参数 `T`,以及一个参数 `arg`,它是一个 `T` 数组,并返回一个 `T` 数组。”如果我们传入一个数字数组,我们将得到一个数字数组,因为 `T` 将绑定到 `number`。这允许我们将泛型类型变量 `T` 用作我们正在使用的类型的部分,而不是整个类型,从而提供更大的灵活性。
我们可以用这种方式编写示例代码
tsTry
functionloggingIdentity <T >(arg :Array <T >):Array <T > {console .log (arg .length ); // Array has a .length, so no more errorreturnarg ;}
你可能已经熟悉了其他语言中的这种类型风格。在下一节中,我们将介绍如何创建自己的泛型类型,例如 `Array<T>`。
泛型类型
在前面的章节中,我们创建了适用于多种类型的通用身份函数。在本节中,我们将探讨函数本身的类型以及如何创建通用接口。
通用函数的类型与非通用函数的类型相同,类型参数列在最前面,类似于函数声明
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : <T >(arg :T ) =>T =identity ;
我们也可以在类型中使用不同的名称来表示通用类型参数,只要类型变量的数量以及类型变量的使用方式一致即可。
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : <U >(arg :U ) =>U =identity ;
我们也可以将通用类型写成对象字面量类型的调用签名
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : { <T >(arg :T ):T } =identity ;
这将引导我们编写第一个通用接口。让我们将前面的示例中的对象字面量移到一个接口中
tsTry
interfaceGenericIdentityFn {<T >(arg :T ):T ;}functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity :GenericIdentityFn =identity ;
在类似的示例中,我们可能希望将通用参数移到整个接口的参数中。这让我们可以查看我们对哪些类型进行泛化(例如 Dictionary<string>
而不是 Dictionary
)。这使得类型参数对接口的所有其他成员可见。
tsTry
interfaceGenericIdentityFn <T > {(arg :T ):T ;}functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity :GenericIdentityFn <number> =identity ;
请注意,我们的示例已经改变,变得略有不同。我们不再描述一个通用函数,而是一个非通用函数签名,它是通用类型的一部分。当我们使用 GenericIdentityFn
时,我们现在还需要指定相应的类型参数(这里:number
),有效地锁定底层调用签名将使用的类型。理解何时将类型参数直接放在调用签名上,以及何时将类型参数放在接口本身,将有助于描述类型的哪些方面是通用的。
除了通用接口,我们还可以创建通用类。请注意,无法创建通用枚举和命名空间。
泛型类
泛型类与泛型接口的形状类似。泛型类在类名之后用尖括号 (<>
) 包含一个通用类型参数列表。
tsTry
classGenericNumber <T > {zeroValue :T ;add : (x :T ,y :T ) =>T ;}letmyGenericNumber = newGenericNumber <number>();myGenericNumber .zeroValue = 0;myGenericNumber .add = function (x ,y ) {returnx +y ;};
这是一个对 GenericNumber
类的非常直接的使用,但你可能已经注意到,没有任何东西限制它只能使用 number
类型。我们也可以使用 string
甚至更复杂的对象。
tsTry
letstringNumeric = newGenericNumber <string>();stringNumeric .zeroValue = "";stringNumeric .add = function (x ,y ) {returnx +y ;};console .log (stringNumeric .add (stringNumeric .zeroValue , "test"));
就像接口一样,将类型参数放在类本身可以确保类的所有属性都使用相同的类型。
正如我们在关于类的部分中所述,类在其类型方面有两个方面:静态方面和实例方面。泛型类只在其实例方面而不是其静态方面是泛型的,因此在使用类时,静态成员不能使用类的类型参数。
泛型约束
如果你还记得之前的一个例子,你可能有时想编写一个泛型函数,它作用于一组类型,你对这组类型将具有的功能有一些了解。在我们的loggingIdentity
示例中,我们希望能够访问arg
的.length
属性,但编译器无法证明每种类型都具有.length
属性,因此它会警告我们不能做出这种假设。
tsTry
functionloggingIdentity <T >(arg :T ):T {Property 'length' does not exist on type 'T'.2339Property 'length' does not exist on type 'T'.console .log (arg .); length returnarg ;}
我们不想使用任何和所有类型,而是希望将此函数限制为作用于任何和所有也具有.length
属性的类型。只要类型具有此成员,我们就允许它,但它至少需要具有此成员。为此,我们必须将我们的要求列为对 T 可以是什么的约束。
为此,我们将创建一个接口来描述我们的约束。在这里,我们将创建一个具有单个.length
属性的接口,然后我们将使用此接口和extends
关键字来表示我们的约束
tsTry
interfaceLengthwise {length : number;}functionloggingIdentity <T extendsLengthwise >(arg :T ):T {console .log (arg .length ); // Now we know it has a .length property, so no more errorreturnarg ;}
由于泛型函数现在受到约束,它将不再适用于任何和所有类型
tsTry
Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.2345Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.loggingIdentity (3 );
相反,我们需要传入类型具有所有必需属性的值
tsTry
loggingIdentity ({length : 10,value : 3 });
在泛型约束中使用类型参数
你可以声明一个受另一个类型参数约束的类型参数。例如,这里我们想从一个对象中获取一个属性,给出它的名称。我们希望确保我们不会意外地获取一个不存在于obj
上的属性,因此我们将在这两种类型之间放置一个约束
tsTry
functiongetProperty <T ,K extends keyofT >(obj :T ,key :K ) {returnobj [key ];}letx = {a : 1,b : 2,c : 3,d : 4 };getProperty (x , "a");Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.2345Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.getProperty (x ,"m" );
在泛型中使用类类型
在使用泛型创建 TypeScript 工厂时,需要通过它们的构造函数引用类类型。例如,
tsTry
functioncreate <T >(c : { new ():T }):T {return newc ();}
一个更高级的示例使用原型属性来推断和约束构造函数和类类型实例侧之间的关系。
tsTry
classBeeKeeper {hasMask : boolean;}classZooKeeper {nametag : string;}classAnimal {numLegs : number;}classBee extendsAnimal {keeper :BeeKeeper ;}classLion extendsAnimal {keeper :ZooKeeper ;}functioncreateInstance <A extendsAnimal >(c : new () =>A ):A {return newc ();}createInstance (Lion ).keeper .nametag ;createInstance (Bee ).keeper .hasMask ;