泛型

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

¥A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you the most flexible capabilities for building up large software systems.

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

¥In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

你好泛型世界

¥Hello World of Generics

首先,让我们做泛型的 “你好世界”:恒等函数。恒等函数是一个函数,它将返回传入的任何内容。你可以将其视为与 echo 命令类似的方式。

¥To start off, let’s do the “hello world” of generics: the identity function. The identity function is a function that will return back whatever is passed in. You can think of this in a similar way to the echo command.

如果没有泛型,我们要么必须给标识函数一个特定的类型:

¥Without generics, we would either have to give the identity function a specific type:

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

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

¥Or, we could describe the identity function using the any type:

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

虽然使用 any 肯定是泛型的,因为它会导致函数接受 arg 类型的任何和所有类型,但实际上我们正在丢失函数返回时该类型的信息。如果我们传入一个数字,我们拥有的唯一信息是可以返回任何类型。

¥While using any is certainly generic in that it will cause the function to accept any and all types for the type of arg, we actually are losing the information about what that type was when the function returns. If we passed in a number, the only information we have is that any type could be returned.

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

¥Instead, we need a way of capturing the type of the argument in such a way that we can also use it to denote what is being returned. Here, we will use a type variable, a special kind of variable that works on types rather than values.

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

我们现在已经向标识函数添加了一个类型变量 Type。这个 Type 允许我们捕获用户提供的类型(例如 number),以便我们以后可以使用该信息。在这里,我们再次使用 Type 作为返回类型。通过检查,我们现在可以看到参数和返回类型使用了相同的类型。这允许我们在函数的一侧和另一侧传输该类型的信息。

¥We’ve now added a type variable Type to the identity function. This Type allows us to capture the type the user provides (e.g. number), so that we can use that information later. Here, we use Type again as the return type. On inspection, we can now see the same type is used for the argument and the return type. This allows us to traffic that type information in one side of the function and out the other.

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

¥We say that this version of the identity function is generic, as it works over a range of types. Unlike using any, it’s also just as precise (i.e., it doesn’t lose any information) as the first identity function that used numbers for the argument and return type.

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

¥Once we’ve written the generic identity function, we can call it in one of two ways. The first way is to pass all of the arguments, including the type argument, to the function:

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

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

¥Here we explicitly set Type to be string as one of the arguments to the function call, denoted using the <> around the arguments rather than ().

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

¥The second way is also perhaps the most common. Here we use type argument inference — that is, we want the compiler to set the value of Type for us automatically based on the type of the argument we pass in:

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

请注意,我们不必在尖括号 (<>) 中显式传递类型;编译器只查看值 "myString",并将 Type 设置为其类型。虽然类型参数推断可以成为保持代码更短和更具可读性的有用工具,但是当编译器无法推断类型时,你可能需要像我们在上一个示例中所做的那样显式传递类型参数,这可能会发生在更复杂的示例中.

¥Notice that we didn’t have to explicitly pass the type in the angle brackets (<>); the compiler just looked at the value "myString", and set Type to its type. While type argument inference can be a helpful tool to keep code shorter and more readable, you may need to explicitly pass in the type arguments as we did in the previous example when the compiler fails to infer the type, as may happen in more complex examples.

使用泛型类型变量

¥Working with Generic Type Variables

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

¥When you begin to use generics, you’ll notice that when you create generic functions like identity, the compiler will enforce that you use any generically typed parameters in the body of the function correctly. That is, that you actually treat these parameters as if they could be any and all types.

让我们使用之前的 identity 函数:

¥Let’s take our identity function from earlier:

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

如果我们还想在每次调用时将参数 arg 的长度记录到控制台怎么办?我们可能会忍不住这样写:

¥What if we want to also log the length of the argument arg to the console with each call? We might be tempted to write this:

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 成员。

¥When we do, the compiler will give us an error that we’re using the .length member of arg, but nowhere have we said that arg has this member. Remember, we said earlier that these type variables stand in for any and all types, so someone using this function could have passed in a number instead, which does not have a .length member.

假设我们实际上打算让这个函数在 Type 的数组上工作,而不是直接在 Type 上工作。由于我们使用的是数组,所以 .length 成员应该可用。我们可以像创建其他类型的数组一样来描述它:

¥Let’s say that we’ve actually intended this function to work on arrays of Type rather than Type directly. Since we’re working with arrays, the .length member should be available. We can describe this just like we would create arrays of other types:

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

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

¥You can read the type of loggingIdentity as “the generic function loggingIdentity takes a type parameter Type, and an argument arg which is an array of Types, and returns an array of Types.” If we passed in an array of numbers, we’d get an array of numbers back out, as Type would bind to number. This allows us to use our generic type variable Type as part of the types we’re working with, rather than the whole type, giving us greater flexibility.

我们也可以这样编写示例示例:

¥We can alternatively write the sample example this way:

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>

¥You may already be familiar with this style of type from other languages. In the next section, we’ll cover how you can create your own generic types like Array<Type>.

泛型类型

¥Generic Types

在前面的部分中,我们创建了适用于一系列类型的泛型恒等函数。在本节中,我们将探讨函数本身的类型以及如何创建泛型接口。

¥In previous sections, we created generic identity functions that worked over a range of types. In this section, we’ll explore the type of the functions themselves and how to create generic interfaces.

泛型函数的类型和非泛型函数的类型一样,类型参数先列出,类似于函数声明:

¥The type of generic functions is just like those of non-generic functions, with the type parameters listed first, similarly to function declarations:

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

我们也可以为类型中的泛型类型参数使用不同的名称,只要类型变量的数量和类型变量的使用方式一致。

¥We could also have used a different name for the generic type parameter in the type, so long as the number of type variables and how the type variables are used line up.

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

我们还可以将泛型类型写为对象字面量类型的调用签名:

¥We can also write the generic type as a call signature of an object literal type:

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

这导致我们编写我们的第一个泛型接口。让我们从前面的示例中获取对象字面量并将其移动到接口:

¥Which leads us to writing our first generic interface. Let’s take the object literal from the previous example and move it to an interface:

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

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

¥In a similar example, we may want to move the generic parameter to be a parameter of the whole interface. This lets us see what type(s) we’re generic over (e.g. Dictionary<string> rather than just Dictionary). This makes the type parameter visible to all the other members of the interface.

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

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

¥Notice that our example has changed to be something slightly different. Instead of describing a generic function, we now have a non-generic function signature that is a part of a generic type. When we use GenericIdentityFn, we now will also need to specify the corresponding type argument (here: number), effectively locking in what the underlying call signature will use. Understanding when to put the type parameter directly on the call signature and when to put it on the interface itself will be helpful in describing what aspects of a type are generic.

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

¥In addition to generic interfaces, we can also create generic classes. Note that it is not possible to create generic enums and namespaces.

泛型类

¥Generic Classes

泛型类具有与泛型接口相似的形状。泛型类在类名称后面的尖括号 (<>) 中有一个泛型类型参数列表。

¥A generic class has a similar shape to a generic interface. Generic classes have a generic type parameter list in angle brackets (<>) following the name of the class.

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 甚至更复杂的对象。

¥This is a pretty literal use of the GenericNumber class, but you may have noticed that nothing is restricting it to only use the number type. We could have instead used string or even more complex objects.

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

就像接口一样,将类型参数放在类本身可以让我们确保类的所有属性都使用相同的类型。

¥Just as with interface, putting the type parameter on the class itself lets us make sure all of the properties of the class are working with the same type.

正如我们在 我们的类章节 中介绍的那样,一个类的类型有两个方面:静态端和实例端。泛型类仅在其实例方面而非其静态方面是泛型的,因此在使用类时,静态成员不能使用类的类型参数。

¥As we cover in our section on classes, a class has two sides to its type: the static side and the instance side. Generic classes are only generic over their instance side rather than their static side, so when working with classes, static members can not use the class’s type parameter.

泛型约束

¥Generic Constraints

如果你还记得前面的示例,你有时可能想要编写一个适用于一组类型的泛型函数,你知道该组类型将具有哪些功能。在我们的 loggingIdentity 示例中,我们希望能够访问 arg.length 属性,但编译器无法证明每种类型都有 .length 属性,所以它警告我们不能做出这个假设。

¥If you remember from an earlier example, you may sometimes want to write a generic function that works on a set of types where you have some knowledge about what capabilities that set of types will have. In our loggingIdentity example, we wanted to be able to access the .length property of arg, but the compiler could not prove that every type had a .length property, so it warns us that we can’t make this assumption.

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 的约束。

¥Instead of working with any and all types, we’d like to constrain this function to work with any and all types that also  have the .length property. As long as the type has this member, we’ll allow it, but it’s required to have at least this member. To do so, we must list our requirement as a constraint on what Type can be.

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

¥To do so, we’ll create an interface that describes our constraint. Here, we’ll create an interface that has a single .length property and then we’ll use this interface and the extends keyword to denote our constraint:

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

因为泛型函数现在受到约束,它将不再适用于任何和所有类型:

¥Because the generic function is now constrained, it will no longer work over any and all types:

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

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

¥Instead, we need to pass in values whose type has all the required properties:

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

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

¥Using Type Parameters in Generic Constraints

你可以声明受另一个类型参数约束的类型参数。例如,在这里我们想从一个给定名称的对象中获取一个属性。We’我想确保我们’ 不会意外获取 obj 上不存在的属性,因此我们将在两种类型之间放置约束:

¥You can declare a type parameter that is constrained by another type parameter. For example, here we’d like to get a property from an object given its name. We’d like to ensure that we’re not accidentally grabbing a property that does not exist on the obj, so we’ll place a constraint between the two types:

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

在泛型中使用类类型

¥Using Class Types in Generics

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

¥When creating factories in TypeScript using generics, it is necessary to refer to class types by their constructor functions. For example,

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

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

¥A more advanced example uses the prototype property to infer and constrain relationships between the constructor function and the instance side of class types.

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

此模式用于为 混入 设计模式提供动力。

¥This pattern is used to power the mixins design pattern.

泛型参数默认值

¥Generic Parameter Defaults

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

¥By declaring a default for a generic type parameter, you make it optional to specify the corresponding type argument. For example, a function which creates a new HTMLElement. Calling the function with no arguments generates a HTMLDivElement; calling the function with an element as the first argument generates an element of the argument’s type. You can optionally pass a list of children as well. Previously you would have to define the function as:

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

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

¥With generic parameter defaults we can reduce it to:

ts
declare function create<T extends HTMLElement = HTMLDivElement, U extends HTMLElement[] = 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

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

¥A generic parameter default follows the following rules:

  • 如果一个类型参数有一个默认值,它就被认为是可选的。

    ¥A type parameter is deemed optional if it has a default.

  • 必需的类型参数不能跟在可选的类型参数之后。

    ¥Required type parameters must not follow optional type parameters.

  • 类型参数的默认类型必须满足类型参数的约束(如果存在)。

    ¥Default types for a type parameter must satisfy the constraint for the type parameter, if it exists.

  • 指定类型参数时,只需为需要的类型参数指定类型参数即可。未指定的类型参数将解析为其默认类型。

    ¥When specifying type arguments, you are only required to specify type arguments for the required type parameters. Unspecified type parameters will resolve to their default types.

  • 如果指定了默认类型并且推断无法选择候选者,则推断默认类型。

    ¥If a default type is specified and inference cannot choose a candidate, the default type is inferred.

  • 与现有类或接口声明合并的类或接口声明可能会为现有类型参数引入默认值。

    ¥A class or interface declaration that merges with an existing class or interface declaration may introduce a default for an existing type parameter.

  • 与现有类或接口声明合并的类或接口声明可以引入新的类型参数,只要它指定默认值即可。

    ¥A class or interface declaration that merges with an existing class or interface declaration may introduce a new type parameter as long as it specifies a default.

方差注释

¥Variance Annotations

这是一个用于解决非常具体问题的高级功能,仅应在你已确定使用它的理由的情况下使用

¥This is an advanced feature for solving a very specific problem, and should only be used in situations where you’ve identified a reason to use it

协变和逆变 是类型理论术语,描述两种泛型类型之间的关系。以下是该概念的简要介绍。

¥Covariance and contravariance are type theory terms that describe what the relationship between two generic types is. Here’s a brief primer on the concept.

例如,如果你有一个表示可以 make 特定类型的对象的接口:

¥For example, if you have an interface representing an object that can make a certain type:

ts
interface Producer<T> {
make(): T;
}

我们可以在需要 Producer<Animal> 的地方使用 Producer<Cat>,因为 CatAnimal。这种关系称为协变:从 Producer<T>Producer<U> 的关系与从 TU 的关系相同。

¥We can use a Producer<Cat> where a Producer<Animal> is expected, because a Cat is an Animal. This relationship is called covariance: the relationship from Producer<T> to Producer<U> is the same as the relationship from T to U.

相反,如果你有一个可以 consume 某种类型的接口:

¥Conversely, if you have an interface that can consume a certain type:

ts
interface Consumer<T> {
consume: (arg: T) => void;
}

然后我们可以在需要 Consumer<Cat> 的地方使用 Consumer<Animal>,因为任何能够接受 Animal 的函数也必须能够接受 Cat。这种关系称为逆变:从 Consumer<T>Consumer<U> 的关系与从 UT 的关系相同。请注意与协方差相比方向的反转!这就是为什么逆变 “自行取消” 而协变不的原因。

¥Then we can use a Consumer<Animal> where a Consumer<Cat> is expected, because any function that is capable of accepting an Animal must also be capable of accepting a Cat. This relationship is called contravariance: the relationship from Consumer<T> to Consumer<U> is the same as the relationship from U to T. Note the reversal of direction as compared to covariance! This is why contravariance “cancels itself out” but covariance doesn’t.

在像 TypeScript 这样的结构类型系统中,协变和逆变是从类型定义中自然出现的行为。即使在没有泛型的情况下,我们也会看到协变(和逆变)关系:

¥In a structural type system like TypeScript’s, covariance and contravariance are naturally emergent behaviors that follow from the definition of types. Even in the absence of generics, we would see covariant (and contravariant) relationships:

ts
interface AnimalProducer {
make(): Animal;
}
// A CatProducer can be used anywhere an
// Animal producer is expected
interface CatProducer {
make(): Cat;
}

TypeScript 有一个结构类型系统,因此在比较两种类型时,例如,要查看是否可以在需要 Producer<Animal> 的地方使用 Producer<Cat>,通常的算法是结构性扩展这两个定义,并比较它们的结构。但是,方差允许进行极其有用的优化:如果 Producer<T>T 上是协变的,那么我们可以简单地检查 CatAnimal,因为我们知道它们将具有与 Producer<Cat>Producer<Animal> 相同的关系。

¥TypeScript has a structural type system, so when comparing two types, e.g. to see if a Producer<Cat> can be used where a Producer<Animal> is expected, the usual algorithm would be structurally expand both of those definitions, and compare their structures. However, variance allows for an extremely useful optimization: if Producer<T> is covariant on T, then we can simply check Cat and Animal instead, as we know they’ll have the same relationship as Producer<Cat> and Producer<Animal>.

请注意,只有在我们检查相同类型的两个实例时才能使用此逻辑。如果我们有 Producer<T>FastProducer<U>,则不能保证 TU 一定引用这些类型中的相同位置,因此此检查将始终在结构上执行。

¥Note that this logic can only be used when we’re examining two instantiations of the same type. If we have a Producer<T> and a FastProducer<U>, there’s no guarantee that T and U necessarily refer to the same positions in these types, so this check will always be performed structurally.

因为方差是结构类型的自然出现属性,所以 TypeScript 会自动推断每个泛型类型的方差。在涉及某些循环类型的极少数情况下,这种测量可能不准确。如果发生这种情况,你可以向类型参数添加方差注释以强制特定方差:

¥Because variance is a naturally emergent property of structural types, TypeScript automatically infers the variance of every generic type. In extremely rare cases involving certain kinds of circular types, this measurement can be inaccurate. If this happens, you can add a variance annotation to a type parameter to force a particular variance:

ts
// Contravariant annotation
interface Consumer<in T> {
consume: (arg: T) => void;
}
// Covariant annotation
interface Producer<out T> {
make(): T;
}
// Invariant annotation
interface ProducerConsumer<in out T> {
consume: (arg: T) => void;
make(): T;
}

仅当你编写的方差应该在结构上发生时才这样做。

¥Only do this if you are writing the same variance that should occur structurally.

永远不要编写与结构方差不匹配的方差注释!

¥Never write a variance annotation that doesn’t match the structural variance!

强调方差注释仅在基于实例化的比较期间有效至关重要。它们在结构比较期间不起作用。例如,你不能使用方差注释来 “force” 类型实际上是不变的:

¥It’s critical to reinforce that variance annotations are only in effect during an instantiation-based comparison. They have no effect during a structural comparison. For example, you can’t use variance annotations to “force” a type to be actually invariant:

ts
// DON'T DO THIS - variance annotation
// does not match structural behavior
interface Producer<in out T> {
make(): T;
}
// Not a type error -- this is a structural
// comparison, so variance annotations are
// not in effect
const p: Producer<string | number> = {
make(): number {
return 42;
}
}

在这里,对象字面量的 make 函数返回 number,我们可能会预期它会导致错误,因为 number 不是 string | number。但是,这不是基于实例的比较,因为对象字面量是匿名类型,而不是 Producer<string | number>

¥Here, the object literal’s make function returns number, which we might expect to cause an error because number isn’t string | number. However, this isn’t an instantiation-based comparison, because the object literal is an anonymous type, not a Producer<string | number>.

变体注释不会改变结构行为,并且仅在特定情况下被参考

¥Variance annotations don’t change structural behavior and are only consulted in specific situations

只有在你完全知道为什么要这样做、它们的局限性是什么以及它们何时不起作用时,才编写方差注释非常重要。TypeScript 是否使用基于实例的比较或结构比较不是指定的行为,并且可能因正确性或性能原因而因版本而异,因此你应该只在变体注释与类型的结构行为匹配时编写它们。不要使用方差注释来尝试 “force” 特定方差;这会导致你的代码出现不可预测的行为。

¥It’s very important to only write variance annotations if you absolutely know why you’re doing it, what their limitations are, and when they aren’t in effect. Whether TypeScript uses an instantiation-based comparison or structural comparison is not a specified behavior and may change from version to version for correctness or performance reasons, so you should only ever write variance annotations when they match the structural behavior of a type. Don’t use variance annotations to try to “force” a particular variance; this will cause unpredictable behavior in your code.

除非方差注释与类型的结构行为相匹配,否则不要编写方差注释

¥Do NOT write variance annotations unless they match the structural behavior of a type

请记住,TypeScript 可以自动从你的泛型类型中推断方差。几乎从来不需要编写方差注释,只有在确定特定需求时才应该这样做。变体注释不会改变类型的结构行为,并且根据情况,当你期望进行基于实例的比较时,你可能会看到进行结构比较。方差注释不能用于修改类型在这些结构上下文中的行为方式,除非注释与结构定义相同,否则不应编写。因为这很难做到正确,并且 TypeScript 可以在绝大多数情况下正确推断方差,所以你不应该在普通代码中编写方差注释。

¥Remember, TypeScript can automatically infer variance from your generic types. It’s almost never necessary to write a variance annotation, and you should only do so when you’ve identified a specific need. Variance annotations do not change the structural behavior of a type, and depending on the situation, you might see a structural comparison made when you expected an instantiation-based comparison. Variance annotations can’t be used to modify how types behave in these structural contexts, and shouldn’t be written unless the annotation is the same as the structural definition. Because this is difficult to get right, and TypeScript can correctly infer variance in the vast majority of cases, you should not find yourself writing variance annotations in normal code.

不要尝试使用方差注释来更改类型检查行为;这不是它们的用途

¥Don’t try to use variance annotations to change typechecking behavior; this is not what they are for

你可能会发现临时方差注释在 “类型调试” 情况下很有用,因为方差注释会被检查。如果注释的方差明显错误,TypeScript 将触发错误:

¥You may find temporary variance annotations useful in a “type debugging” situation, because variance annotations are checked. TypeScript will issue an error if the annotated variance is identifiably wrong:

ts
// Error, this interface is definitely contravariant on T
interface Foo<out T> {
consume: (arg: T) => void;
}

但是,方差注释可以更严格(例如,如果实际方差是协变的,则 in out 有效)。调试完成后,请务必删除方差注释。

¥However, variance annotations are allowed to be stricter (e.g. in out is valid if the actual variance is covariant). Be sure to remove your variance annotations once you’re done debugging.

最后,如果你试图最大化你的类型检查性能,并且已经运行了分析器,并且已经确定了速度慢的特定类型,并且已经确定了方差推断速度特别慢,并且已经仔细验证了你想要编写的方差注释,你可能会看到通过添加方差注释,在极其复杂的类型中性能略有提高。

¥Lastly, if you’re trying to maximize your typechecking performance, and have run a profiler, and have identified a specific type that’s slow, and have identified variance inference specifically is slow, and have carefully validated the variance annotation you want to write, you may see a small performance benefit in extraordinarily complex types by adding variance annotations.

不要尝试使用方差注释来更改类型检查行为;这不是它们的用途

¥Don’t try to use variance annotations to change typechecking behavior; this is not what they are for