Appearance
泛型
软件工程的一个主要部分是建立组件,这些组件不仅有定义明确和一致的 API,而且还可以重复使用。能够处理今天的数据和明天的数据的组件将为你建立大型软件系统提供最灵活的能力。
在像 C# 和 Java 这样的语言中,创建可重用组件的工具箱中的主要工具之一是泛型,也就是说,能够创建一个在各种类型上工作的组件,而不是单一的类型。这使得用户可以消费这些组件并使用他们自己的类型。
Hello World
首先,让我们做一下泛型的 " hello world":身份函数。身份函数是一个函数,它将返回传入的任何内容。你可以用类似于 echo 命令的方式来考虑它。 如果没有泛型,我们将不得不给身份函数一个特定的类型。 或者,我们可以用任意类型来描述身份函数。
typeScript
function identity(arg: any): any {
return arg;
}
使用 any 当然是通用的,因为它将使函数接受 arg 类型的任何和所有的类型,但实际上我们在函数返回时失去了关于该类型的信息。如果我们传入一个数字,我们唯一的信息就是任何类型都可以被返回。
相反,我们需要一种方法来捕获参数的类型,以便我们也可以用它来表示返回的内容。在这里,我们将使用一个类型变量,这是一种特殊的变量,对类型而不是数值起作用。
typeScript
function identity<Type>(arg: Type): Type {
return arg;
}
我们现在已经在身份函数中添加了一个类型变量 Type 。这个 Type 允许我们捕获用户提供的类型(例如数字),这样我们就可以在以后使用这些信息。这里,我们再次使用 Type 作为返回类型。经过检查,我们现在可以看到参数和返回类型使用的是相同的类型。这使得我们可以将类型信息从函数的一侧输入,然后从另一侧输出。
我们说这个版本的身份函数是通用的,因为它在一系列的类型上工作。与使用任何类型不同的是,它也和第一个使用数字作为参数和返回类型的身份函数一样精确(即,它不会丢失任何信息)。
一旦我们写好了通用身份函数,我们就可以用两种方式之一来调用它。第一种方式是将所有的参数,包括类型参数,都传递给函数:
typeScript
let output = identity<string>("myString");
这里我们明确地将 Type 设置为 string ,作为函数调用的参数之一,用参数周围的 <> 而不是 () 来表示。
第二种方式可能也是最常见的。这里我们使用类型参数推理——也就是说,我们希望编译器根据我们传入的参数的类型,自动为我们设置 Type 的值。
typeScript
let output = identity("myString");
注意,我们不必在角括号(<>)中明确地传递类型;编译器只是查看了 "myString" 这个值,并将 Type 设置为其类型。
虽然类型参数推断是一个有用的工具,可以使代码更短、更易读,但当编译器不能推断出类型时,你可能需要像我们在前面的例子中那样明确地传入类型参数,这在更复杂的例子中可能发生。
使用通用类型变量
当你开始使用泛型时,你会注意到,当你创建像 identity 这样的泛型函数时,编译器会强制要求你在函数主体中正确使用任何泛型参数。也就是说,你实际上是把这些参数当作是任何和所有的类型。
让我们来看看我们前面的 identity 函数。
typeScript
function identity<Type>(arg: Type): Type {
return arg;
}
如果我们想在每次调用时将参数 arg 的长度记录到控制台,该怎么办?我们可能很想这样写:
typeScript
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
当我们这样做时,编译器会给我们一个错误,说我们在使用 arg 的 .length 成员,但我们没有说 arg 有这个成员。记住,我们在前面说过,这些类型的变量可以代表任何和所有的类型,所以使用这个函数的人可以传入一个 number ,而这个数字没有一个 .length 成员。
比方说,我们实际上是想让这个函数在 Type 的数组上工作,而不是直接在 Type 上工作。既然我们在处理数组,那么 .length 成员应该是可用的。我们可以像创建其他类型的数组那样来描述它。
typescript
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
你可以把 loggingIdentity 的类型理解为 "通用函数 loggingIdentity 接收一个类型参数 Type 和 一个参数 arg , arg 是一个 Type 数组,并返回一个 Type 数组。"
如果我们传入一个数字数组,我们会得到一个数字数组,因为 Type 会绑定到数字。这允许我们使用我们的通用类型变量 Type 作为我们正在处理的类型的一部分,而不是整个类型,给我们更大的灵活性。
我们也可以这样来写这个例子:
typescript
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // 数组有一个.length,所以不会再出错了
return arg;
}
你可能已经从其他语言中熟悉了这种类型的风格。在下一节中,我们将介绍如何创建你自己的通用类型
如 Array<Type>
泛型类型
在前几节中,我们创建了在一系列类型上工作的通用身份函数。在这一节中,我们将探讨函数本身的类型以及如何创建通用接口。
泛型函数的类型与非泛型函数的类型一样,类型参数列在前面,与函数声明类似:
typescript
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;
我们也可以为类型中的通用类型参数使用一个不同的名字,只要类型变量的数量和类型变量的使用方式一致。
typescript
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Input>(arg: Input) => Input = identity;
我们也可以把泛型写成一个对象字面类型的调用签名。
typescript
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;
这让我们开始编写我们的第一个泛型接口。让我们把前面例子中的对象字面意思移到一个接口中。
typescript
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
在一个类似的例子中,我们可能想把通用参数移到整个接口的参数上。这可以让我们看到我们的泛型是什么类型(例如, Dictionary<string> 而不是仅仅 Dictionary )。这使得类型参数对接口的所有其他成员可见。
typeScript
interface GenericIdentityFn<Type> { (arg: Type): Type; }
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
请注意,我们的例子已经改变了,变成了稍微不同的东西。我们现在没有描述一个泛型函数,而是有一个非泛型的函数签名,它是泛型类型的一部分。当我们使用 GenericIdentityFn 时,我们现在还需要指定相应的类型参数(这里是:数字),有效地锁定了底层调用签名将使用什么。
了解什么时候把类型参数直接放在调用签名上,什么时候把它放在接口本身,将有助于描述一个类型的哪些方面是通用的。
除了泛型接口之外,我们还可以创建泛型类。注意,不可能创建泛型枚举和命名空间。
泛型类
一个泛型类的形状与泛型接口相似。泛型类在类的名字后面有一个角括号(<>)中的泛型参数列表。
typeScript
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;
};
这是对 GenericNumber 类相当直白的使用,但你可能已经注意到,没有任何东西限制它只能使用数字类型。我们本可以使用字符串或更复杂的对象。
typeScript
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
}
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
就像接口一样,把类型参数放在类本身,可以让我们确保类的所有属性都与相同的类型一起工作。
正如我们在关于类的章节中提到的,一个类的类型有两个方面:静态方面和实例方面。通用类只在其实例侧而非静态侧具有通用性,所以在使用类时,静态成员不能使用类的类型参数。
泛型约束
如果你还记得前面的例子,你有时可能想写一个通用函数,在一组类型上工作,而你对这组类型会有什么能力有一定的了解。在我们的 loggingIdentity 例子中,我们希望能够访问 arg.length 属性,但是编译器无法证明每个类型都有一个 .length 属性,所以它警告我们不能做这个假设。
typeScript
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
我们希望限制这个函数与 any 和所有类型一起工作,而不是与 any 和所有同时具有 .length 属性的类型一起工作。只要这个类型有这个成员,我们就允许它,但它必须至少有这个成员。要做到这一点,我们必须把我们的要求作为一个约束条件列在 Type 可以是什么。
为了做到这一点,我们将创建一个接口来描述我们的约束。在这里,我们将创建一个接口,它有一个单一的 .length 属性,然后我们将使用这个接和 extends 关键字来表示我们的约束条件。
typeScript
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // 现在我们知道它有一个 .length 属性,所以不再有错误了
return arg;
}
因为泛型函数现在被限制了,它将不再对 any 和所有的类型起作用。
typeScript
loggingIdentity(3);
相反,我们需要传入其类型具有所有所需属性的值。
typeScript
loggingIdentity({ length: 10, value: 3 });
在泛型约束中使用类型参数
你可以声明一个受另一个类型参数约束的类型参数。例如,在这里我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取一个不存在于 obj 上的属性,所以我们要在这两种类型之间放置一个约束条件。
typeScript
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
在泛型中使用类类型
在 TypeScript 中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型。比如说:
typescript
function create<Type>(c: { new (): Type }): Type {
return new c();
}
一个更高级的例子,使用原型属性来推断和约束类类型的构造函数和实例方之间的关系。
typescript
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;