当一个类implements 是一个接口时,它实际上并不影响类的类型。编译器检查该类与接口兼容,但它不使用接口作为上下文来为类的成员提供类型。如果您编写 class Foo implements Bar {...} 并且没有编译器错误,那么编译器会将其视为与您省略 implements Bar 并仅编写 class Foo {...} 一样。有关详细信息,请参阅 microsoft/TypeScript#32082 和其中链接的问题。
所以BaseClass 的controls 属性被推断为具有空对象类型{}。您可能期望BaseClass 的controls 属性为{ [key: string] : number } 类型,但这不会发生,因为此类信息仅来自implements IBase 子句,在为BaseClass 提供类型时会忽略该子句的成员。如果您想查看实现类或子类具有特定类型的属性,您应该对它们进行注释:
class BaseClass implements IBase {
controls: { [key: string]: number } = {}; // annotated
}
class TestClass extends BaseClass {
// you might also want to annotate this, depending on intent
controls = { test: 'a' }; // error!
}
同样重要的是要注意,无论好坏,TypeScript 的类型系统都不是完全的sound;有一些“漏洞”,TypeScript 允许不一致的赋值。
在健全的类型系统中,subtyping 应该是transitive;这意味着,对于任何类型 A、B 和 C,如果是 A extends B 和 B extends C,则为 A extends C。虽然这在 TypeScript 中很常见,但有时会被违反。这就是您的示例中发生的情况:TestClass extends BaseClass 和 BaseClass extends IBase,但 TestClass 没有扩展 IBase。
如果我们创建一个仅在 T extends U 时编译的辅助类型函数 VerifyExtends<T, U>,我们可以见证这种子类型化的不传递性:
type VerifyExtends<T extends U, U> = void;
type AB = VerifyExtends<TestClass, BaseClass> // okay
type BC = VerifyExtends<BaseClass, IBase> // okay
type AC = VerifyExtends<TestClass, IBase> // error!
TestClass extends BaseClass 因为TestClass 中的controls 属性具有BaseClass ({}) 中没有提到的额外属性,并且允许您通过添加属性来扩展对象类型。
还有BaseClass extends TestClass,因为它的controls 属性{} 具有与索引签名冲突的属性(它根本没有属性)......并且TypeScript 会将implicit index signatures 赋予这些类型。
当然,TestClass 不会扩展 IBase,因为额外的属性和隐式索引签名不是相互一致的。所以我们有奇怪的地方。
在您的情况下,我可能会建议显式注释,因为这是您的意图。但是你迟早会遇到不健全的,你应该为此做好准备。
Playground link to code