Generics
泛型
介绍
软件工程的主要部分是构建不仅具有定义良好且一致的API的组件,而且还可重用。能够处理当前数据以及明天数据的组件将为您提供构建大型软件系统的最灵活的功能。
在像C#和Java这样的语言中,用于创建可重用组件的工具箱中的主要工具之一是泛型
,也就是说,能够创建可以处理多种类型的组件的组件,而不是单个组件。这允许用户使用这些组件并使用它们自己的类型。
泛型的Hello World
首先,我们来做一下泛型的“hello world”:身份函数。身份函数是一个函数,它会返回任何传入的函数。您可以用与echo
命令类似的方式来思考它。
没有泛型,我们要么必须给身份函数一个特定的类型:
function identity(arg: number): number {
return arg;
}
或者,我们可以使用以下any
类型来描述身份函数:
function identity(arg: any): any {
return arg;
}
虽然使用any
肯定是泛型的,它会导致函数接受任何类型的类型arg
,但实际上我们正在丢失函数返回时类型的信息。如果我们传入一个数字,我们唯一的信息就是可以返回任何类型的数据。
相反,我们需要一种捕捉参数类型的方式,以便我们也可以用它来表示返回的内容。在这里,我们将使用一个类型变量
,它是一种特殊类型的变量,它用于类型而不是值。
function identity<T>(arg: T): T {
return arg;
}
我们现在已经为T
身份函数添加了一个类型变量。这T
允许我们捕获用户提供的类型(例如number
),以便我们稍后可以使用该信息。在这里,我们T
再次用作返回类型。在检查时,我们现在可以看到用于参数和返回类型的相同类型。这使我们能够在功能的一侧和另一侧输入该类型的信息。
我们说这个版本的identity
函数是通用的,因为它可以处理一系列的类型。与使用不同any
,它也是精确的(也就是说,它不会丢失任何信息)作为identity
使用数字作为参数和返回类型的第一个函数。
一旦我们编写了通用标识函数,我们可以通过以下两种方式之一来调用它。第一种方法是将所有参数(包括类型参数)传递给函数:
let output = identity<string>("myString" // type of output will be 'string'
在这里,我们明确地将其设置T为string函数调用的参数之一,用参数<>周围表示而不是()。
第二种方式也许是最常见的。在这里我们使用类型参数推理
- 也就是说,我们希望编译器T
根据我们传入的参数的类型自动设置我们的值:
let output = identity("myString" // type of output will be 'string'
请注意,我们不必在尖括号(<>)中显式传递类型; 编译器只是看着这个值"myString",并设置T为它的类型。虽然类型自变量推断可能是一个有助于保持代码更短且更易读的工具,但您可能需要显式传入类型参数,正如我们在前面的示例中所做的那样,当编译器无法推断类型时,可能会发生在更复杂的示例中。
使用泛型类型变量
当你开始使用泛型时,你会注意到当你创建泛型函数时identity
,编译器会强制你正确地在函数体中使用任何泛型类型的参数。也就是说,您实际上将这些参数视为可以是任何类型和所有类型。
让我们identity
先从我们的函数:
function identity<T>(arg: T): T {
return arg;
}
如果我们还想arg
每次调用时将参数的长度记录到控制台,该怎么办?我们可能会试着写这个:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length // Error: T doesn't have .length
return arg;
}
当我们这样做时,编译器会给我们一个我们正在使用该.length
成员的错误arg
,但是我们arg
并没有说过这个成员。请记住,我们之前说过,这些类型变量支持任何类型,所以使用此函数的用户可能已经传入了一个number
没有.length
成员的变量。
比方说,我们实际上希望这个函数能够在数组上T
而不是T
直接操作。由于我们正在使用数组,所以.length
成员应该可用。我们可以像描述其他类型的数组一样描述它:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length // Array has a .length, so no more error
return arg;
}
你可以读取类型loggingIdentity
为“泛型函数loggingIdentity
接受一个类型参数T
,并且一个参数arg
是一个T
s 数组,并且返回一个T
s 数组” 。如果我们传入一个数组数组,我们会得到一个数组数字退出,如同T
绑定number
。这使我们可以使用泛型类型变量T
作为我们正在使用的类型的一部分,而不是整个类型,从而为我们提供了更大的灵活性。
我们也可以用这种方式编写示例示例:
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length // Array has a .length, so no more error
return arg;
}
您可能已经熟悉来自其他语言的这种类型的风格。在下一节中,我们将介绍如何创建自己的泛型类型Array<T>。
泛型类型
在前面的章节中,我们创建了一系列类型的通用身份函数。在本节中,我们将探讨函数本身的类型以及如何创建通用接口。
泛型函数的类型与非泛型函数的类型类似,首先列出类型参数,类似于函数声明:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
只要类型变量的数量和类型变量如何排列,我们也可以为类型中的泛型类型参数使用不同的名称。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
我们也可以将泛型类型写成对象文字类型的调用签名:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;
这导致我们编写我们的第一个通用接口。让我们从前面的例子中获取对象字面值并将其移动到一个接口:
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
在一个类似的例子中,我们可能想要将泛型参数作为整个接口的参数。这让我们看到了我们通用的类型(例如,Dictionary<string>而不仅仅是Dictionary)。这使得类型参数对接口的所有其他成员都可见。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
请注意,我们的例子已经改变为略有不同的东西。我们现在不用描述泛型函数,而是使用属于泛型类型的非泛型函数签名。当我们使用时GenericIdentityFn
,我们现在还需要指定相应的类型参数(这里number
:),有效地锁定底层调用签名将使用的内容。了解何时将类型参数直接放在呼叫签名上以及何时将其放在接口本身上将有助于描述类型的哪些方面是通用的。
除了通用接口之外,我们还可以创建泛型类。请注意,不可能创建泛型枚举和名称空间。
泛型类
泛型类与泛型接口具有相似的形状。泛型类在类的名称后面的尖括号(<>)中有一个泛型类型参数列表。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>(
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
这是类的一个非常类似的用法GenericNumber
,但您可能已经注意到没有任何限制它仅使用该number
类型。我们可以使用string
或更复杂的对象。
let stringNumeric = new GenericNumber<string>(
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
alert(stringNumeric.add(stringNumeric.zeroValue, "test")
就像接口一样,将类型参数放在类本身上让我们确保类的所有属性都使用相同的类型。
正如我们在类中所讨论的,类有两个方面:静态和实例。泛型类只是泛型的,而不是它们的静态方面,所以当使用类时,静态成员不能使用类的类型参数。
通用约束
如果你从前面的例子中还记得,你可能有时想写一个通用函数,它对一组类型有一定的了解,这些类型的类型将具有哪些功能。在我们的loggingIdentity
例子中,我们希望能够访问.length
属性arg
,但编译器无法证明每种类型都有.length
属性,所以它警告我们不能做出这样的假设。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length // Error: T doesn't have .length
return arg;
}
我们不想使用任何和所有类型,而是想限制这个函数与任何和所有类型一起工作.length
。只要类型包含此成员,我们将允许它,但至少需要此成员。要做到这一点,我们必须列出我们的要求,作为T可以成为的约束条件。
为此,我们将创建一个描述我们约束的接口。在这里,我们将创建一个具有单个.length
属性的接口,然后我们将使用此接口和extends
关键字来表示我们的约束:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length // Now we know it has a .length property, so no more error
return arg;
}
由于泛型函数现在受到限制,它将不再适用于任何类型的所有类型:
loggingIdentity(3 // Error, number doesn't have a .length property
相反,我们需要传入其类型具有所有必需属性的值:
loggingIdentity{length: 10, value: 3}
在通用约束中使用类型参数
您可以声明受另一个类型参数约束的类型参数。例如,在这里我们希望从名称中获得一个对象的属性。我们希望确保我们不会意外地抓住一个不存在的属性obj
,所以我们会在这两种类型之间设置一个约束:
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" // okay
getProperty(x, "m" // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
在泛型中使用类类型
在使用泛型在TypeScript中创建工厂时,需要通过构造函数引用类类型。例如,
function create<T>(c: {new(): T; }): T {
return new c(
}
更高级的示例使用prototype属性来推断和约束构造函数和类类型的实例端之间的关系。
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c(
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!