介绍
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 中广泛使用匿名对象,例如函数表达式和对象字面量,因此最好使用结构类型系统来描述此类类型,而不是名义类型系统。
可靠性注意事项
TypeScript 的类型系统允许在编译时无法确认安全的单个操作。 当类型系统具有此属性时,它被认为是“不可靠的”。 TypeScript 允许这些不可靠行为的方式是经过深思熟虑的。 通过这篇文章,我们解释了这种情况何时发生及其好处。
开始
TypeScript 结构化类型系统的一个基本规则是,如果 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中的每个属性typescript 类型拓展,看看y中是否也能找到对应的属性。 在这种情况下,y必须包含一个名称为name的字符串类型成员。 y 满足条件,因此形参正确。
检查函数参数时使用相同的规则:
function greet(n: Named) {
alert('Hello, ' + n.name);
}
greet(y); // OK
请注意,y 有一个附加的位置属性,但这不会导致错误。 仅对目标类型(此处命名)的成员进行一一检查兼容性。
该比较过程是递归执行的,检查每个成员和子成员。
比较两个函数
相对而言,比较原始类型和对象类型更容易理解。 问题是如何判断两个功能兼容。 让我们从两个简单的函数开始,它们的参数列表仅略有不同:
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 向回调函数传递 3 个参数:数组元素、索引和整个链表。 尽管如此,传入仅接受第一个参数的回调函数可能会很有用:
let items = [1, 2, 3];
// Don't force these extra arguments
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));
可选参数和剩余参数
当比较函数的兼容性时,可选参数和必需参数是可以互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型中没有对应的参数也不是错误。
当函数有剩余参数时,它被视为无限数量的可选参数。
这对于类型系统来说是不稳定的,但是从运行时的角度来看,可选参数通常不会强制执行,因为对于大多数函数来说,它相当于传递一些 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));
函数重载
对于具有重载的函数,源函数的每次重载都必须在目标函数上找到相应的函数签名。 这确保了只要源函数可调用,目标函数就可调用。
枚举
枚举类型与数字类型兼容,数字类型与枚举类型兼容。 不同枚举类型之间不存在兼容性。 例如,
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 类型拓展,但不允许传递给相同类型的其他类。
通用的
由于 TypeScript 是结构类型系统,因此类型参数仅影响将它们用作类型一部分的结果类型。 例如,
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // okay, y matches structure of x
在上面的代码中,x和y是兼容的,因为在使用类型参数时它们的结构没有不同。 更改此示例以添加成员以查看其工作原理:
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 中,有两种类型的兼容性:子类型和参数。 它们的区别在于,赋值扩展了子类型兼容性,允许任何参数传入或传出任何值,并允许数字参数传入枚举类型或枚举类型参数传入数字。
语言中的不同地方使用各自的机制。 事实上,类型兼容性是由形式参数兼容性控制的,即使是在implements和extends语句中也是如此。 有关更多信息,请参阅 TypeScript 语言规范。