类型兼容性

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 的类型系统允许某些在编译时不知道的操作是安全的。当一个类型系统有这个属性时,就说它不是 “sound”。仔细考虑了 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.

但是请注意,对象字面量 只能指定已知属性。例如,因为我们明确指定了 dogPet 类型,所以下面的代码是无效的:

¥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 中的 ‘discarding’ 参数。允许这种赋值的原因是忽略额外的函数参数实际上在 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

到目前为止,我们使用的是 “compatible”,这不是语言规范中定义的术语。在 TypeScript 中,有两种兼容性:子类型和赋值。这些不同之处仅在于分配扩展了与规则的子类型兼容性,以允许分配到 any 和从 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 关闭时兼容的组合。

¥The following table summarizes assignability between some abstract types. Rows indicate what each is assignable to, columns indicate what is assignable to them. A ”” indicates a combination that is compatible only when strictNullChecks is off.

any unknown 对象 void undefined null never
any →
unknown →
对象 →
void →
undefined →
null →
never →

重申 基础知识

¥Reiterating The Basics:

  • 一切都可以分配给它自己。

    ¥Everything is assignable to itself.

  • anyunknown 在可分配给它们的方面是相同的,不同之处在于 unknown 不能分配给除 any 之外的任何东西。

    ¥any and unknown are the same in terms of what is assignable to them, different in that unknown is not assignable to anything except any.

  • unknownnever 就像彼此的倒数。一切都可以分配给 unknownnever 可以分配给一切。没有任何东西可以分配给 neverunknown 不能分配给任何东西(any 除外)。

    ¥unknown and never are like inverses of each other. Everything is assignable to unknown, never is assignable to everything. Nothing is assignable to never, unknown is not assignable to anything (except any).

  • void 不可分配给任何东西或从任何东西分配,但以下情况除外:anyunknownneverundefinednull(如果 strictNullChecks 关闭,请参见表格了解详细信息)。

    ¥void is not assignable to or from anything, with the following exceptions: any, unknown, never, undefined, and null (if strictNullChecks is off, see table for details).

  • strictNullChecks 关闭时,nullundefinednever 类似:可分配给大多数类型,大多数类型不可分配给它们。它们可以相互分配。

    ¥When strictNullChecks is off, null and undefined are similar to never: assignable to most types, most types are not assignable to them. They are assignable to each other.

  • strictNullChecks 打开时,nullundefined 的行为更像 void:除了 anyunknownnevervoidundefined 总是可以分配给 void)之外,不能分配给任何东西,也不能分配给任何东西。

    ¥When strictNullChecks is on, null and undefined behave more like void: not assignable to or from anything, except for any, unknown, never, and void (undefined is always assignable to void).