TypeScript 中的类型兼容性基于结构子类型。 结构类型是一种仅基于其成员关联类型的方法。这与 nominal typing 相反。考虑以下代码:
interface Pet {
name: string;
}
class Dog {
name: string;
}
let pet: Pet;
// OK, because of structural typing
pet = new Dog();
如果是 Java 或者 ABAP 编程语言,上述代码会出现编译错误,因为 Pet 类型 和 Dog 类型没有任何关联,虽然其正好都有一个类型为 string 的 name 字段。
在 C# 或 Java 等名义类型的语言中,上述代码将是错误的,因为 Dog 类没有明确地将自己描述为 Pet 接口的实现者。
TypeScript 的结构类型系统是根据 JavaScript 代码的典型编写方式设计的。 因为 JavaScript 广泛使用匿名对象,如函数表达式和对象字面量,所以用结构类型系统而不是名义类型系统来表示 JavaScript 库中发现的各种关系要自然得多。
什么是健全性 Soundness
TypeScript 的类型系统允许某些在编译时无法知道的操作是安全的。 当一个类型系统具有这个属性时,它就被认为不是“健全的”。 仔细考虑了 TypeScript 允许不良行为的地方,在整个文档中,我们将解释这些地方发生的地方以及它们背后的激励场景。
TypeScript 结构类型系统的基本规则是,如果 y 至少具有与 x 相同的成员,则 x 与 y 兼容。 例如,考虑以下代码,其中包含一个名为 Pet 的接口,该接口具有 name 属性。这段代码是合法的:
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 的成员,它是一个字符串。 确实如此,因此允许分配。
同理,下列代码也合法:
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)。
如何判断两个函数类型是否兼容
虽然比较原始类型和对象类型相对简单,但哪些类型的函数应该被视为兼容的问题更复杂一些。 让我们从两个仅参数列表不同的函数的基本示例开始
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
请注意 dog 有一个额外的 owner 属性,但这不会产生错误。 检查兼容性时只考虑目标类型的成员(在本例中为 Pet)。
如何判断两个函数类型是否兼容
虽然比较原始类型和对象类型相对简单,但哪些类型的函数应该被视为兼容的问题更复杂一些。 让我们从两个仅参数列表不同的函数的基本示例开始:
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