Type Compatibility
类型兼容性(Type Compatibility)
介绍
TypeScript 中的类型兼容性基于结构化子类型。结构类型是仅基于成员关联类型的一种方式。这与名义类型相反。考虑下面的代码:
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
// OK, because of structural typing
p = new Person(
在 C# 或 Java 等名义类型的语言中,等效的代码将是一个错误,因为Person
类没有明确地将自己描述为Named
接口的实现者。
TypeScript 的结构类型系统是基于通常编写 JavaScript 代码的方式设计的。由于 JavaScript 广泛使用匿名对象(如函数表达式和对象文字),因此使用结构类型系统而不是名义类型代表 JavaScript 库中存在的各种关系更为自然。
关于健全性的一个注记
TypeScript 的类型系统允许某些在编译时无法知道的操作是安全的。当一个类型系统有这个属性时,它被认为不是“健全的”。TypeScript 允许不正确的行为的地方被仔细考虑过了,在本文中我们将解释这些发生的地方以及它们背后的动机场景。
开始
Ty
peScript 的结构类型系统的基本规则x
是与y
如果 y
至少具有相同的成员x
。例如:
interface Named {
name: string;
}
let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
x = y;
为了检查是否y
可以分配x
,编译器检查每个属性以x
在其中找到相应的兼容属性y
。在这种情况下,y
必须有一个名为的成员name
是一个字符串。它的确如此,因此分配是允许的。
检查函数调用参数时使用相同的赋值规则:
function greet(n: Named) {
alert("Hello, " + n.name
}
greet(y // OK
请注意,它y
有一个额外的location
属性,但这不会产生错误。Named
在检查兼容性时,只考虑目标类型的成员(在这种情况下)。
该比较过程递归地进行,探索每个成员和子成员的类型。
比较两个功能
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:
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
要检查是否x
可分配y
,我们首先查看参数列表。每个参数都x
必须有y
一个兼容类型的相应参数。请注意,不考虑参数的名称,只考虑它们的类型。在这种情况下,每个参数x
都有一个相应的兼容参数y
,因此赋值是允许的。
第二个赋值是一个错误,因为y有一个'x'没有的必需的第二个参数,所以这个赋值是不允许的。
您可能想知道为什么我们允许像在示例中那样“丢弃”参数y = x
。允许这种赋值的原因是忽略额外的函数参数在 JavaScript 中实际上非常普遍。例如,Array#forEach
为回调函数提供三个参数:数组元素,其索引和包含数组。不过,提供仅使用第一个参数的回调会非常有用:
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)
现在让我们看看如何处理返回类型,使用两个函数,它们只有返回类型不同:
let x = () => {name: "Alice"}
let y = () => {name: "Alice", location: "Seattle"}
x = y; // OK
y = x; // Error because x() lacks a location property
类型系统强制源函数的返回类型是目标类型返回类型的子类型。
函数参数双变量
在比较函数参数的类型时,如果源参数可分配给目标参数,则分配成功,反之亦然。这是不合适的,因为调用者最终可能会得到一个采用更专门化类型的函数,但会调用具有较少专用类型的函数。实际上,这种错误很少见,并且允许使用许多常见的 JavaScript 模式。一个简单的例子:
enum EventType { Mouse, Keyboard }
interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* ... */
}
// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + "," + e.y)
// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + "," + (<MouseEvent>e).y)
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + "," + e.y))
// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e)
可选参数和其余参数
在比较兼容性功能时,可选和必需的参数是可以互换的。源类型的额外可选参数不是错误,并且源类型中没有相应参数的目标类型的可选参数不是错误。
当一个函数有一个 rest 参数时,它被当作是无限的一系列可选参数。
从类型系统的角度来看,这是不合理的,但是从运行时的角度来看,可选参数的思想一般不会很好实施,因为通过undefined
这个位置对于大多数函数来说是等价的。
这个动机的例子是一个函数的通用模式,该函数接受一个回调,并用一些可预测的(对程序员)但是对类型系统不知道的参数数量调用它:
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)
带过载的功能
当函数有重载时,源类型中的每个重载必须与目标类型上的兼容签名匹配。这确保了目标函数可以在所有与源函数相同的情况下被调用。
Enums
枚举与数字兼容,并且数字与枚举兼容。来自不同枚举类型的枚举值被认为是不兼容的。例如,
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let status = Status.Ready;
status = Color.Green; //error
类
类与对象文字类型和接口的工作方式类似,但有一个例外:它们同时具有静态类型和实例类型。比较类类型的两个对象时,只比较实例的成员。静态成员和构造函数不影响兼容性。
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
类的私有与受限成员
班级中的私人和受保护的成员会影响其兼容性。当检查类的实例的兼容性时,如果目标类型包含私有成员,则源类型还必须包含源自相同类的私有成员。同样,对于具有受保护成员的实例也是如此。这允许一个类与其超类的赋值兼容,但不能
与其他具有相同形状的继承层次结构的类相匹配。
泛型
因为 TypeScript 是一个结构类型系统,所以类型参数仅在作为成员类型的一部分使用时影响结果类型。例如,
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // okay, y matches structure of x
在上面,x并且y是兼容的,因为它们的结构不以区分方式使用类型参数。通过添加一个成员来更改这个例子来Empty<T>说明这是如何工作的:
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // error, x and y are not compatible
通过这种方式,具有指定类型参数的泛型类型就像非泛型类型一样。
对于没有指定类型参数的泛型类型,通过指定any
所有未指定的类型参数来检查兼容性。然后检查结果类型的兼容性,就像在非泛型情况下一样。
例如,
let identity = function<T>(x: T): T {
// ...
}
let reverse = function<U>(y: U): U {
// ...
}
identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any
高级主题
子类型与分配
到目前为止,我们已经使用'兼容',这不是在语言规范中定义的术语。在 TypeScript 中,有两种兼容性:子类型和赋值。这些不同之处仅在于赋值扩展了子类型与规则的兼容性,以允许将any
枚举与相应的数值进行分配,以及从枚举到和从枚举分配。
语言中的不同位置根据情况使用两种兼容性机制之一。出于实用目的,即使在implements
和extends
子句的情况下,类型兼容性也由赋值兼容性决定。有关更多信息,请参阅 TypeScript 规范
。