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 typingpet = 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
相同的成员,则 x
与 y
兼容。例如,考虑以下代码,其中涉及一个名为 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; // OKx = 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 parametersitems.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; // OKy = 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 commonlistenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y));// Undesirable alternatives in presence of soundnesslistenEvent(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 typeslistenEvent(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 argumentsinvokeLater([1, 2], (x, y) => console.log(x + ", " + y));// Confusing (x and y are actually required) and undiscoverableinvokeLater([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; // OKs = 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
在上面,x
和 y
是兼容的,因为它们的结构不以区分方式使用类型参数。通过向 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.
语言中不同的地方使用两种兼容机制中的一种,视情况而定。出于实际目的,类型兼容性由赋值兼容性决定,即使在 implements
和 extends
子句的情况下也是如此。
¥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.
any
、unknown
、object
、void
、undefined
、null
和 never
可赋值性
¥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.
-
any
和unknown
在可分配给它们的方面是相同的,不同之处在于unknown
不能分配给除any
之外的任何东西。¥
any
andunknown
are the same in terms of what is assignable to them, different in thatunknown
is not assignable to anything exceptany
. -
unknown
和never
就像彼此的倒数。一切都可以分配给unknown
,never
可以分配给一切。没有任何东西可以分配给never
,unknown
不能分配给任何东西(any
除外)。¥
unknown
andnever
are like inverses of each other. Everything is assignable tounknown
,never
is assignable to everything. Nothing is assignable tonever
,unknown
is not assignable to anything (exceptany
). -
void
不可分配给任何东西或从任何东西分配,但以下情况除外:any
、unknown
、never
、undefined
和null
(如果strictNullChecks
关闭,请参见表格了解详细信息)。¥
void
is not assignable to or from anything, with the following exceptions:any
,unknown
,never
,undefined
, andnull
(ifstrictNullChecks
is off, see table for details). -
当
strictNullChecks
关闭时,null
和undefined
与never
类似:可分配给大多数类型,大多数类型不可分配给它们。它们可以相互分配。¥When
strictNullChecks
is off,null
andundefined
are similar tonever
: assignable to most types, most types are not assignable to them. They are assignable to each other. -
当
strictNullChecks
打开时,null
和undefined
的行为更像void
:除了any
、unknown
和void
(undefined
始终可以分配给void
)之外,不能分配给任何东西。¥When
strictNullChecks
is on,null
andundefined
behave more likevoid
: not assignable to or from anything, except forany
,unknown
, andvoid
(undefined
is always assignable tovoid
).