泛型

软件工程的一个重要部分是构建不仅具有定义明确且一致的 API,而且可重用的组件。能够处理今天和明天数据的组件将为您构建大型软件系统提供最灵活的功能。

在 C# 和 Java 等语言中,创建可重用组件的主要工具之一是泛型,即能够创建可以在各种类型而不是单个类型上工作的组件。这允许用户使用这些组件并使用他们自己的类型。

泛型入门

首先,让我们来学习泛型的“hello world”:恒等函数。恒等函数会返回传入的任何值。你可以把它理解成类似于echo命令。

在没有泛型的情况下,我们必须为恒等函数指定一个特定的类型

ts
function identity(arg: number): number {
return arg;
}
Try

或者,我们可以使用any类型来描述恒等函数

ts
function identity(arg: any): any {
return arg;
}
Try

虽然使用any确实很通用,因为它可以让函数接受任何类型的arg,但实际上我们丢失了函数返回时关于该类型的信息。如果我们传入一个数字,我们唯一知道的信息是它可以返回任何类型。

相反,我们需要一种方法来捕获参数的类型,以便我们也可以用它来表示返回值的类型。在这里,我们将使用一个类型变量,这是一种特殊类型的变量,它作用于类型而不是值。

ts
function identity<Type>(arg: Type): Type {
return arg;
}
Try

我们现在在恒等函数中添加了一个类型变量Type。这个Type允许我们捕获用户提供的类型(例如number),以便我们稍后使用该信息。在这里,我们再次使用Type作为返回类型。通过检查,我们可以看到参数和返回类型使用了相同的类型。这使我们能够在函数的一侧传递类型信息,并在另一侧接收它。

我们说这个版本的identity函数是泛型的,因为它适用于多种类型。与使用any不同,它也与第一个使用数字作为参数和返回类型的identity函数一样精确(即,它不会丢失任何信息)。

一旦我们编写了通用的身份函数,就可以通过两种方式调用它。第一种方式是将所有参数(包括类型参数)传递给函数

ts
let output = identity<string>("myString");
let output: string
Try

这里,我们显式地将Type设置为string,作为函数调用参数之一,使用<>表示参数,而不是()

第二种方式也许是最常见的。在这里,我们使用*类型参数推断*——也就是说,我们希望编译器根据我们传入的参数类型自动为我们设置Type的值

ts
let output = identity("myString");
let output: string
Try

请注意,我们不需要在尖括号(<>)中显式地传递类型;编译器只需查看值"myString",并将Type设置为其类型。虽然类型参数推断可以帮助简化代码并提高可读性,但在编译器无法推断类型的情况下,您可能需要像在前面的示例中那样显式地传递类型参数,这种情况可能发生在更复杂的示例中。

使用泛型类型变量

当您开始使用泛型时,您会注意到,当您创建像identity这样的泛型函数时,编译器会强制您在函数体中正确使用任何泛型类型参数。也就是说,您实际上应该将这些参数视为任何类型。

让我们从之前的identity函数开始

ts
function identity<Type>(arg: Type): Type {
return arg;
}
Try

如果我们想在每次调用时将参数arg的长度也记录到控制台,我们可能会想这样写

ts
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
Property 'length' does not exist on type 'Type'.2339Property 'length' does not exist on type 'Type'.
return arg;
}
Try

当我们这样做时,编译器会给我们一个错误,说我们正在使用 `arg` 的 `.length` 成员,但我们并没有在任何地方说过 `arg` 有这个成员。记住,我们之前说过这些类型变量代表任何和所有类型,所以使用这个函数的人可能会传入一个 `number`,它没有 `.length` 成员。

假设我们实际上是想让这个函数作用于 `Type` 的数组,而不是直接作用于 `Type` 本身。由于我们正在处理数组,`.length` 成员应该可用。我们可以像创建其他类型的数组一样描述它

ts
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
Try

你可以将 `loggingIdentity` 的类型理解为“泛型函数 `loggingIdentity` 接受一个类型参数 `Type`,以及一个参数 `arg`,它是一个 `Type` 数组,并返回一个 `Type` 数组。”如果我们传入一个数字数组,我们会得到一个数字数组作为返回值,因为 `Type` 会绑定到 `number`。这允许我们将泛型类型变量 `Type` 用作我们正在处理的类型的组成部分,而不是整个类型,从而获得更大的灵活性。

我们可以用另一种方式编写示例代码

ts
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
Try

你可能已经熟悉其他语言中的这种类型风格。在下一节中,我们将介绍如何创建自己的泛型类型,例如 `Array<Type>`。

泛型类型

在前面的章节中,我们创建了适用于多种类型的泛型标识函数。在本节中,我们将探讨这些函数本身的类型以及如何创建泛型接口。

泛型函数的类型与非泛型函数的类型相同,只是类型参数列在前面,类似于函数声明

ts
function identity<Type>(arg: Type): Type {
return arg;
}
 
let myIdentity: <Type>(arg: Type) => Type = identity;
Try

我们也可以在类型中使用不同的名称来表示泛型类型参数,只要类型变量的数量以及类型变量的使用方式一致即可。

ts
function identity<Input>(arg: Input): Input {
return arg;
}
 
let myIdentity: <Input>(arg: Input) => Input = identity;
Try

我们也可以将泛型类型写成对象字面量类型的调用签名

ts
function identity<Type>(arg: Type): Type {
return arg;
}
 
let myIdentity: { <Type>(arg: Type): Type } = identity;
Try

这将引导我们编写第一个泛型接口。让我们将前面的示例中的对象字面量移到一个接口中

ts
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
return arg;
}
 
let myIdentity: GenericIdentityFn = identity;
Try

在类似的例子中,我们可能希望将泛型参数移动到整个接口的参数。这让我们可以查看我们泛化的类型(例如 Dictionary<string> 而不是仅仅 Dictionary)。这使得类型参数对接口的所有其他成员可见。

ts
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
return arg;
}
 
let myIdentity: GenericIdentityFn<number> = identity;
Try

请注意,我们的示例已更改为略有不同。我们不再描述泛型函数,而是拥有一个非泛型函数签名,它是泛型类型的一部分。当我们使用 GenericIdentityFn 时,我们现在还需要指定相应的类型参数(这里:number),有效地锁定底层调用签名将使用的内容。了解何时将类型参数直接放在调用签名上以及何时将其放在接口本身将有助于描述类型的哪些方面是泛型的。

除了泛型接口之外,我们还可以创建泛型类。请注意,无法创建泛型枚举和命名空间。

泛型类

泛型类与泛型接口具有类似的形状。泛型类在类名之后用尖括号 (<>) 具有泛型类型参数列表。

ts
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;
};
Try

这是一个对 GenericNumber 类的非常字面的使用,但您可能已经注意到,没有任何东西限制它只能使用 number 类型。我们本可以使用 string 甚至更复杂的对象。

ts
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
 
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
Try

与接口一样,将类型参数放在类本身允许我们确保类的所有属性都使用相同的类型。

正如我们在 关于类的部分 中所述,类在其类型方面有两个方面:静态方面和实例方面。泛型类仅在其实例方面而不是其静态方面是泛型的,因此在使用类时,静态成员不能使用类的类型参数。

泛型约束

如果您还记得之前的示例,您有时可能希望编写一个泛型函数,该函数适用于您对该类型集具有一些了解的类型集,即该类型集将具有哪些功能。在我们的 loggingIdentity 示例中,我们希望能够访问 arg.length 属性,但编译器无法证明每种类型都具有 .length 属性,因此它警告我们不能做出此假设。

ts
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
Property 'length' does not exist on type 'Type'.2339Property 'length' does not exist on type 'Type'.
return arg;
}
Try

与其处理任何类型,我们希望将此函数限制为处理所有具有.length属性的类型。只要类型具有此成员,我们就会允许它,但它必须至少具有此成员。为此,我们必须将我们的要求列为对Type可以是什么的约束。

为此,我们将创建一个接口来描述我们的约束。在这里,我们将创建一个具有单个.length属性的接口,然后我们将使用此接口和extends关键字来表示我们的约束。

ts
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;
}
Try

由于泛型函数现在受到约束,它将不再适用于任何类型。

ts
loggingIdentity(3);
Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.2345Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
Try

相反,我们需要传入类型具有所有必需属性的值。

ts
loggingIdentity({ length: 10, value: 3 });
Try

在泛型约束中使用类型参数

您可以声明一个受另一个类型参数约束的类型参数。例如,这里我们希望从对象中获取其名称的属性。我们希望确保我们不会意外地获取obj上不存在的属性,因此我们将在两种类型之间设置约束。

ts
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");
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"'.
Try

在泛型中使用类类型

在使用泛型创建 TypeScript 工厂时,需要通过它们的构造函数引用类类型。例如,

ts
function create<Type>(c: { new (): Type }): Type {
return new c();
}
Try

一个更高级的示例使用原型属性来推断和约束构造函数和类类型实例侧之间的关系。

ts
class BeeKeeper {
hasMask: boolean = true;
}
 
class ZooKeeper {
nametag: string = "Mikle";
}
 
class Animal {
numLegs: number = 4;
}
 
class Bee extends Animal {
numLegs = 6;
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;
Try

此模式用于为mixins设计模式提供支持。

泛型参数默认值

通过为泛型类型参数声明默认值,您可以使指定相应的类型参数成为可选。例如,一个创建新HTMLElement的函数。在不带参数的情况下调用该函数会生成一个HTMLDivElement;在第一个参数中带有一个元素的情况下调用该函数会生成一个与参数类型相同的元素。您也可以选择传递一个子元素列表。以前您必须将函数定义为

ts
declare function create(): Container<HTMLDivElement, HTMLDivElement[]>;
declare function create<T extends HTMLElement>(element: T): Container<T, T[]>;
declare function create<T extends HTMLElement, U extends HTMLElement>(
element: T,
children: U[]
): Container<T, U[]>;
Try

使用泛型参数默认值,我们可以将其简化为

ts
declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(
element?: T,
children?: U
): Container<T, U>;
 
const div = create();
const div: Container<HTMLDivElement, HTMLDivElement[]>
 
const p = create(new HTMLParagraphElement());
const p: Container<HTMLParagraphElement, HTMLParagraphElement[]>
Try

泛型参数默认值遵循以下规则

  • 如果类型参数具有默认值,则该类型参数被视为可选。
  • 必需类型参数不能位于可选类型参数之后。
  • 类型参数的默认类型必须满足类型参数的约束(如果存在)。
  • 在指定类型参数时,您只需要为必需类型参数指定类型参数。未指定的类型参数将解析为其默认类型。
  • 如果指定了默认类型,并且推断无法选择候选类型,则推断默认类型。
  • 与现有类或接口声明合并的类或接口声明可以为现有类型参数引入默认值。
  • 与现有类或接口声明合并的类或接口声明可以引入新的类型参数,只要它指定了默认值。

TypeScript 文档是一个开源项目。帮助我们改进这些页面 通过发送 Pull Request

此页面的贡献者
OTOrta Therox (26)
NKNavneet Karnani (2)
JBJake Bailey (1)
MMFredX (1)
ZSZack Schuster (1)
6+

上次更新:2024 年 3 月 21 日