类型兼容性

TypeScript 中的类型兼容性是基于结构子类型的。结构类型是一种仅根据其成员来关联类型的方式。这与名义类型形成对比。考虑以下代码:

🌐 Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing. Consider the following code:

ts
interface Pet {
name: string;
}
class Dog {
name: string;
}
let pet: Pet;
// OK, because of structural typing
pet = new Dog();

在名义类型语言(如 C# 或 Java)中,等效的代码会出错,因为 Dog 类并没有明确地声明自己实现了 Pet 接口。

🌐 In nominally-typed languages like C# or Java, the equivalent code would be an error because the Dog class does not explicitly describe itself as being an implementer of the Pet interface.

TypeScript 的结构类型系统是基于 JavaScript 代码的典型写法设计的。因为 JavaScript 广泛使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来表示 JavaScript 库中存在的各种关系要比使用名义类型系统自然得多。

🌐 TypeScript’s structural type system was designed based on how JavaScript code is typically written. Because JavaScript widely uses anonymous objects like function expressions and object literals, it’s much more natural to represent the kinds of relationships found in JavaScript libraries with a structural type system instead of a nominal one.

关于健全性的说明

🌐 A Note on Soundness

TypeScript 的类型系统允许某些在编译时无法确定的操作是安全的。当一个类型系统具有这种特性时,它被称为“不健全”。TypeScript 允许不健全行为的地方是经过仔细考虑的,在本文档中,我们将解释这些情况发生的地方以及背后的动机场景。

🌐 TypeScript’s type system allows certain operations that can’t be known at compile-time to be safe. When a type system has this property, it is said to not be “sound”. The places where TypeScript allows unsound behavior were carefully considered, and throughout this document we’ll explain where these happen and the motivating scenarios behind them.

开始

🌐 Starting out

TypeScript 结构化类型系统的基本规则是,如果 y 至少具有与 x 相同的成员,则 xy 是兼容的。例如,考虑以下涉及名为 Pet 的接口的代码,该接口具有一个 name 属性:

🌐 The basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x. For example consider the following code involving an interface named Pet which has a name property:

ts
interface Pet {
name: string;
}
let pet: Pet;
// dog's inferred type is { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog;

要检查 dog 是否可以赋值给 pet,编译器会检查 pet 的每个属性,以在 dog 中找到对应的兼容属性。 在这种情况下,dog 必须有一个名为 name 的成员,并且该成员是字符串类型。它确实有,所以赋值是允许的。

🌐 To check whether dog can be assigned to pet, the compiler checks each property of pet to find a corresponding compatible property in dog. In this case, dog must have a member called name that is a string. It does, so the assignment is allowed.

检查函数调用参数时使用相同的赋值规则:

🌐 The same rule for assignment is used when checking function call arguments:

ts
interface Pet {
name: string;
}
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
function greet(pet: Pet) {
console.log("Hello, " + pet.name);
}
greet(dog); // OK

请注意,dog 有一个额外的 owner 属性,但这不会导致错误。只有目标类型的成员(在本例中为 Pet)在检查兼容性时才会被考虑。这个比较过程是递归进行的,会检查每个成员及子成员的类型。

🌐 Note that dog has an extra owner property, but this does not create an error. Only members of the target type (Pet in this case) are considered when checking for compatibility. This comparison process proceeds recursively, exploring the type of each member and sub-member.

但是,请注意,对象字面量可能只能指定已知属性。例如,因为我们已经显式指定 dog 的类型为 Pet,以下代码是无效的:

🌐 Be aware, however, that object literals may only specify known properties. For example, because we have explicitly specified that dog is of type Pet, the following code is invalid:

ts
let dog: Pet = { name: "Lassie", owner: "Rudd Weatherwax" }; // Error

比较两个函数

🌐 Comparing two functions

虽然比较原始类型和对象类型相对简单,但关于应该认为什么样的函数是兼容的这个问题则复杂一些。让我们先从一个基本的例子开始,两个函数仅在参数列表上有所不同:

🌐 While comparing primitive types and object types is relatively straightforward, the question of what kinds of functions should be considered compatible is a bit more involved. Let’s start with a basic example of two functions that differ only in their parameter lists:

ts
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error

要检查 x 是否可以赋值给 y,我们首先查看参数列表。x 中的每个参数都必须在 y 中有一个类型兼容的对应参数。请注意,这里不考虑参数的名称,只考虑它们的类型。在这种情况下,x 的每个参数在 y 中都有一个兼容的对应参数,因此允许进行赋值。

🌐 To check if x is assignable to y, we first look at the parameter list. Each parameter in x must have a corresponding parameter in y with a compatible type. Note that the names of the parameters are not considered, only their types. In this case, every parameter of x has a corresponding compatible parameter in y, so the assignment is allowed.

第二次赋值是错误的,因为 y 有一个必需的第二个参数,而 x 没有,因此不允许进行赋值。

🌐 The second assignment is an error, because y has a required second parameter that x does not have, so the assignment is disallowed.

你可能想知道为什么我们允许像例子 y = x 中那样“丢弃”参数。允许这种赋值的原因是,忽略额外的函数参数在 JavaScript 中实际上相当常见。例如,Array#forEach 为回调函数提供了三个参数:数组元素、它的索引以及包含它的数组。尽管如此,提供一个只使用第一个参数的回调函数还是非常有用的:

🌐 You may be wondering why we allow ‘discarding’ parameters like in the example y = x. The reason for this assignment to be allowed is that ignoring extra function parameters is actually quite common in JavaScript. For example, Array#forEach provides three parameters to the callback function: the array element, its index, and the containing array. Nevertheless, it’s very useful to provide a callback that only uses the first parameter:

ts
let items = [1, 2, 3];
// Don't force these extra parameters
items.forEach((item, index, array) => console.log(item));
// Should be OK!
items.forEach((item) => console.log(item));

现在让我们看看如何处理返回类型,使用两个仅返回类型不同的函数:

🌐 Now let’s look at how return types are treated, using two functions that differ only by their return type:

ts
let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });
x = y; // OK
y = x; // Error, because x() lacks a location property

类型系统强制源函数的返回类型是目标类型的返回类型的子类型。

🌐 The type system enforces that the source function’s return type be a subtype of the target type’s return type.

函数参数双方差

🌐 Function Parameter Bivariance

在比较函数参数的类型时,如果源参数可以赋值给目标参数,或者反过来,赋值就会成功。这是不安全的,因为调用者最终可能得到一个接受更专门类型的函数,但却用较不专门的类型来调用该函数。实际上,这类错误很少发生,并且允许这样做可以支持许多常见的 JavaScript 写法。简要示例如下:

🌐 When comparing the types of function parameters, assignment succeeds if either the source parameter is assignable to the target parameter, or vice versa. This is unsound because a caller might end up being given a function that takes a more specialized type, but invokes the function with a less specialized type. In practice, this sort of error is rare, and allowing this enables many common JavaScript patterns. A brief example:

ts
enum EventType {
Mouse,
Keyboard,
}
interface Event {
timestamp: number;
}
interface MyMouseEvent extends Event {
x: number;
y: number;
}
interface MyKeyEvent extends Event {
keyCode: number;
}
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* ... */
}
// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y));
// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) =>
console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)
);
listenEvent(EventType.Mouse, ((e: MyMouseEvent) =>
console.log(e.x + "," + e.y)) as (e: Event) => void);
// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

你可以通过编译器标志 strictFunctionTypes 让 TypeScript 在发生这种情况时报错。

🌐 You can have TypeScript raise errors when this happens via the compiler flag strictFunctionTypes.

可选参数和剩余参数

🌐 Optional Parameters and Rest Parameters

在比较函数的兼容性时,可选参数和必需参数是可以互换的。源类型的额外可选参数不是错误,目标类型的可选参数如果在源类型中没有对应的参数也不是错误。

🌐 When comparing functions for compatibility, optional and required parameters are interchangeable. Extra optional parameters of the source type are not an error, and optional parameters of the target type without corresponding parameters in the source type are not an error.

当一个函数有一个剩余参数时,它被视为一个无限系列的可选参数。

🌐 When a function has a rest parameter, it is treated as if it were an infinite series of optional parameters.

从类型系统的角度来看这是不合理的,但从运行时的角度来看,可选参数的概念通常并没有严格执行,因为在大多数函数中,在该位置传入 undefined 是等效的。

🌐 This is unsound from a type system perspective, but from a runtime point of view the idea of an optional parameter is generally not well-enforced since passing undefined in that position is equivalent for most functions.

激励示例是一个函数的常见模式,它接受一个回调并使用一些可预测的(对于程序员)但未知数量的参数(对于类型系统)来调用它:

🌐 The motivating example is the common pattern of a function that takes a callback and invokes it with some predictable (to the programmer) but unknown (to the type system) number of arguments:

ts
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... Invoke callback with 'args' ... */
}
// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));
// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));

具有重载的函数

🌐 Functions with overloads

当一个函数有重载时,目标类型中的每个重载都必须与源类型上的兼容签名相匹配。这确保源函数可以在与目标函数相同的所有情况下被调用。

🌐 When a function has overloads, each overload in the target type must be matched by a compatible signature on the source type. This ensures that the source function can be called in all the same cases as the target function.

枚举

🌐 Enums

枚举与数字兼容,数字也与枚举兼容。不同枚举类型的枚举值被视为不兼容。例如,

🌐 Enums are compatible with numbers, and numbers are compatible with enums. Enum values from different enum types are considered incompatible. For example,

ts
enum Status {
Ready,
Waiting,
}
enum Color {
Red,
Blue,
Green,
}
let status = Status.Ready;
status = Color.Green; // Error

🌐 Classes

类的工作方式类似于对象字面量类型和接口,但有一个例外:它们同时具有静态类型和实例类型。 在比较两个类类型的对象时,只比较实例的成员。 静态成员和构造函数不会影响兼容性。

🌐 Classes work similarly to object literal types and interfaces with one exception: they have both a static and an instance type. When comparing two objects of a class type, only members of the instance are compared. Static members and constructors do not affect compatibility.

ts
class Animal {
feet: number;
constructor(name: string, numFeet: number) {}
}
class Size {
feet: number;
constructor(numFeet: number) {}
}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OK

类中的私有成员和受保护成员

🌐 Private and protected members in classes

类中的私有成员和受保护成员会影响其兼容性。当检查类的实例是否兼容时,如果目标类型包含私有成员,则源类型也必须包含来自同一类的私有成员。同样,对于包含受保护成员的实例也适用相同的规则。这允许一个类与其超类在赋值上兼容,但不与来自不同继承层次且形状相同的类兼容。

🌐 Private and protected members in a class affect their compatibility. When an instance of a class is checked for compatibility, if the target type contains a private member, then the source type must also contain a private member that originated from the same class. Likewise, the same applies for an instance with a protected member. This allows a class to be assignment compatible with its super class, but not with classes from a different inheritance hierarchy which otherwise have the same shape.

泛型

🌐 Generics

因为 TypeScript 是一种结构化类型系统,类型参数只有在作为成员类型的一部分被使用时才会影响最终类型。例如,

🌐 Because TypeScript is a structural type system, type parameters only affect the resulting type when consumed as part of the type of a member. For example,

ts
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, because y matches structure of x

在上例中,xy 是兼容的,因为它们的结构没有以不同方式使用类型参数。通过向 Empty<T> 添加一个成员来改变这个示例,可以显示其工作原理如下:

🌐 In the above, x and y are compatible because their structures do not use the type argument in a differentiating way. Changing this example by adding a member to Empty<T> shows how this works:

ts
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // Error, because x and y are not compatible

这样,指定了类型参数的泛型类型就像非泛型类型一样。

🌐 In this way, a generic type that has its type arguments specified acts just like a non-generic type.

对于没有指定类型参数的泛型类型,通过将所有未指定的类型参数替换为 any 来检查兼容性。然后,对生成的类型进行兼容性检查,就像在非泛型情况下一样。

🌐 For generic types that do not have their type arguments specified, compatibility is checked by specifying any in place of all unspecified type arguments. The resulting types are then checked for compatibility, just as in the non-generic case.

例如,

🌐 For example,

ts
let identity = function <T>(x: T): T {
// ...
};
let reverse = function <U>(y: U): U {
// ...
};
identity = reverse; // OK, because (x: any) => any matches (y: any) => any

高级主题

🌐 Advanced Topics

子类型与赋值

🌐 Subtype vs Assignment

到目前为止,我们使用了“兼容”,这是语言规范中没有定义的术语。在 TypeScript 中,有两种兼容性:子类型兼容性和赋值兼容性。它们的区别仅在于赋值兼容性在子类型兼容性的基础上增加了规则,以允许对 any 的赋值,以及对 enum 及其对应的数值进行赋值。

🌐 So far, we’ve used “compatible”, which is not a term defined in the language spec. In TypeScript, there are two kinds of compatibility: subtype and assignment. These differ only in that assignment extends subtype compatibility with rules to allow assignment to and from any, and to and from enum with corresponding numeric values.

根据情况,语言中的不同地方会使用两种兼容机制中的一种。就实际用途而言,类型兼容性是由赋值兼容性决定的,即使在 implementsextends 条款的情况下也是如此。

🌐 Different places in the language use one of the two compatibility mechanisms, depending on the situation. For practical purposes, type compatibility is dictated by assignment compatibility, even in the cases of the implements and extends clauses.

anyunknownobjectvoidundefinednullnever 的可赋值性

🌐 any, unknown, object, void, undefined, null, and never assignability

下表总结了一些抽象类型之间的可赋值性。行表示每种类型可以赋值给哪些,列表示哪些类型可以赋值给它们。“”表示该组合只有在strictNullChecks关闭时才兼容。

any unknown object void undefined null never
any →
unknown →
object →
void →
undefined →
null →
never →

重申基础知识

🌐 Reiterating The Basics:

  • 一切都可以分配给它自己。
  • anyunknown 在可赋值的内容上是相同的,不同的是 unknown 只能赋值给 any,不能赋值给其他任何东西。
  • unknownnever 就像彼此的逆。所有东西都可以赋值给 unknownnever 可以赋值给所有东西。没有东西可以赋值给 neverunknown 不能赋值给任何东西(除了 any)。
  • void 不能赋值给或从任何东西赋值,但有以下例外:anyunknownneverundefinednull(如果 strictNullChecks 关闭,请参见表格了解详情)。
  • strictNullChecks 关闭时,nullundefined 类似于 never:可以赋值给大多数类型,但大多数类型不能赋值给它们。它们可以相互赋值。
  • strictNullChecks 开启时,nullundefined 的行为更像 void:除了 anyunknownvoid 之外,不能赋值给或从任何其他类型赋值(undefined 始终可以赋值给 void)。