泛型

软件工程的一个重要部分是构建不仅具有明确定义和一致的 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

首先,让我们从泛型的“Hello World”开始:恒等函数。恒等函数是一个会返回传入参数的函数。你可以将其想象成类似于 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 参数,该参数是 Type 的数组,并返回一个 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

你可以声明一个类型参数,它受到另一个类型参数的约束。 例如,这里我们希望根据名称从对象中获取一个属性。 我们希望确保不会意外地获取 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

这种模式用于为 mixins 设计模式提供动力。

🌐 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:

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

方差注释

🌐 Variance Annotations

这是一个用于解决非常特定问题的高级功能,只有在你确定有使用它的理由时才应该使用

[协变和逆变](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) 是类型理论中的术语,用于描述两个泛型类型之间的关系。这里是一个关于这一概念的简要入门介绍。

例如,如果你有一个表示可以对某种类型进行 make 的对象的接口:

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

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

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

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<Cat> 是否可以在期望 Producer<Animal> 的地方使用,通常的算法是对这两个定义进行结构化展开,并比较它们的结构。不过,修改性允许一种非常有用的优化:如果 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.

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

必须强调的是,方差注解仅在基于实例化的比较期间有效。它们在结构比较期间没有作用。例如,你不能使用方差注解来“强制”一个类型实际上是不可变的:

🌐 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;
}
}

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>.

方差注解不会改变结构行为,只在特定情况下才会被参考

只有在你完全清楚自己为什么这么做、它们的限制是什么以及什么时候不起作用时,才书写方差注解,这是非常重要的。TypeScript 是使用实例化比较还是结构比较,并不是规定的行为,并且可能会因正确性或性能原因而随版本变化,因此你应该只在类型的结构行为与之匹配时才写方差注解。不要使用方差注解来尝试“强制”特定的方差,这会导致代码中出现不可预测的行为。

🌐 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.

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

请记住,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.

不要尝试使用修改注解来改变类型检查行为;它们不是用来做这个的

在“类型调试”的情况下,你可能会发现临时方差注解很有用,因为方差注解会被检查。如果标注的方差明显错误,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;
}

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.

不要尝试使用修改注解来改变类型检查行为;它们不是用来做这个的