在这种情况下,TypeScript 类型系统是否过于宽松?

Is the TypeScript type system too relaxed in this case?

我想我遇到了一个似乎应该导致 TS 编译器出错的场景(但实际上并没有),我希望有人能解释原因。

在下面的代码中,我将接口 Foo 传递给它接受的函数 frobnicator。然后,在 frobnicator 的正文中,我“删除”了字段 bar.yfrobnicator 终止后,类型系统允许我打印 bar.y(没有错误),尽管 y 不再存在。

类型系统不应该禁止我将 foo 传递给 frobnicator 因为现在 foo 不再实现 Foo 接口并且 TS 认为它实现了?

interface Foo {
    bar: { x: number, y: number };
}

function frobnicator(thing: { bar: { x: number } }) {
    thing.bar = { x: 1 }; // "remove" bar.y
}

const foo: Foo = { bar: { x: 1, y: 2 } };
frobnicator(foo); // The implementation "removes" bar.y
console.log(foo.bar.y); // TypeScript does not error despite bar.y missing

该函数只需要一个嵌套字典。 不是确切的键数。

你的接口与函数内的代码无关,因为即使你的 const 类型是 Foo,函数接受的不是 foo,而是未命名的类型 { bar: { x: number } }。由于 Typescript 在 编译时 而不是在 运行时 中工作,因此它不可能知道您的函数已删除 y 作为内部类型函数没有 y 并且它外面的类型是 Foo 这是函数参数类型的超类型,因此它的实例适合参数的类型。因此,一方面你的函数从对象中删除了一个字段,但你没有告诉编译器它是被禁止的,因为函数内部的类型与你传递给它的类型不同,所以在外部它仍然认为你有 y。在像 Java 这样的语言之后,这似乎违反直觉,但事实就是如此。为避免此类问题,您应该使用正确的类型,例如,在函数内使用您的接口类型。

答案是肯定的,您的代码展示了 Typescript 类型系统的不正常行为,这正是您在问题中描述的原因。

答案是否定的,你没有“破坏”Typescript 的类型系统,因为这种不合理的行为是预期的行为。打字稿并不完全可靠,也不意味着完全可靠;引用自 the docs:

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.

Typescript 的设计者明确选择了稳健性和实用性之间的平衡;一个完整的类型系统不会将 {bar: {x: number, y: number}} 视为 {bar: {x: number}} 的子类型,但真正的 Javascript 代码通常会传递具有比函数实际访问的属性更多的对象,因此严格来说行为对于编写实际 Javascript 代码的开发人员来说不是很有用。