TypeScript中的接口与Java中的有何区别?

4

在 TypeScript 中会出现错误...

interface A {
    getSomeBasicA():A;
    getSomeBetterA():BetterA;
}

interface BetterA extends A {
    getSomeBasicA():BetterA;
    doFancyAStuff():void;
}

interface B extends A {
    getSomeBetterA():BetterB;
}

interface BetterB extends B, BetterA {}

... with this message ...

error TS2430: Interface 'B' incorrectly extends interface 'A'.
  The types returned by 'getSomeBetterA().getSomeBasicA()' are incompatible between these types.
    Property 'doFancyAStuff' is missing in type 'A' but required in type 'BetterA'.
error TS2320: Interface 'BetterB' cannot simultaneously extend types 'B' and 'BetterA'.
  Named property 'getSomeBasicA' of types 'B' and 'BetterA' are not identical.
error TS2320: Interface 'BetterB' cannot simultaneously extend types 'B' and 'BetterA'.
  Named property 'getSomeBetterA' of types 'B' and 'BetterA' are not identical.

虽然这在Java中可行,但...

public interface A {
    public A getSomeBasicA();
    public BetterA getSomeBetterA();
}

public interface BetterA extends A {
    public BetterA getSomeBasicA();
    public void doFancyAStuff();
}

public interface B extends A {
    public BetterB getSomeBetterA();
}

public interface BetterB extends B, BetterA {
    
}

我不明白为什么会出现这种情况?难道这不是类型安全的(它如何被破坏,为什么Java编译器允许这种情况)?有没有一个名称可以用来描述一个编译器支持而另一个编译器不支持的功能?


据我理解,接口概念在两种语言中只是相似但有很大不同。在TS中,您不需要在任何类上声明接口,编译器只需检查给定的对象是否符合接口中声明的属性即可。这允许与纯JS对象和您无法直接控制的对象进行交互。另一方面,extends关键字主要用于扩展现有接口,而不是通过覆盖来更改其定义。这与编译器本身的关系较小,而与两种语言在运行时的工作方式有关。 - Roland Kreuzer
2
这是TS中的一个限制/缺失功能,请参见microsoft/TypeScript#16936以获取相关功能请求。除非有人先到,否则我会在到达真正的计算机时撰写答案。 - jcalz
2个回答

4
正如您所见,TypeScript 要求在通过逗号符号表示继承多个接口时(例如,interface X extends Y, Z { ... }),任何在父接口中出现重复的成员名称都需要具有相同的类型。

定义 BetterB 违反了此限制;它不能被声明为同时扩展 BBetterAB 有一个类型为 () => AgetBasicA 成员,而 BetterA 有一个类型为 () => BetterAgetBasicA 成员。这些类型不相同,因此会出现错误,并且由于接口定义是递归的,整个过程最终会崩溃。


这个要求是多接口扩展中共同属性具有相同类型,可能比必要的要严格。在microsoft/TypeScript#16936上有一个开放的功能请求,希望允许共同成员具有非相同但“兼容”的类型(大概意思是类型有些重叠)。在这种情况下该如何处理还不是100%清楚;也许新接口的属性类型将是每个父接口属性类型的交集,或者可能会发生更复杂的选择过程。但无论如何,在TS4.1中它都不是语言的一部分。
再次强调:你试图做的不是不安全的类型,只是不支持。

那么,你能做什么? TypeScript的一个显著特点是其类型系统是结构性的,而不是像Java一样是名义上的。在TypeScript中,类型是通过它们的形状而不是它们的声明进行比较的。在Java中,如果你不能声明一个interface X extends Y, Z,那么当你被要求提供一个YZ时,你就不能使用一个X。TypeScript没有这样的限制:如果X的结构与YZ都兼容,即使X的声明没有明确地提到YZ,你也可以用X代替YZ

这意味着,如果我们可以确定BetterB上希望看到的成员名称和类型,我们只需将BetterB定义为具有该形状,而无需明确说明extends B, BetterA。 如果我们做得正确,它将自动扩展这些接口。
我能想到的最简单的方法是:
interface BetterB extends Pick<B, "getSomeBetterA">, BetterA { }

在这里,我们仍在扩展 BetterA,但不是显式地扩展所有的 B,而是使用 Pick 实用类型 来表示我们只想扩展具有 getSomeBetterA 成员的 B 部分。有问题的 getBasicA 方法被排除在外,因此没有错误。由于 BetterAgetBasicA 方法可以赋值给 BgetBasicA 方法,因此 BetterB 仍然在结构上扩展了 B,如下所示:

declare const bB: BetterB;
const b: B = bB; // okay

在将类型为BetterB的值分配给类型为B的变量时不会出现错误。虽然您没有声明BetterB extends B,但它仍然发生了。

代码的游乐场链接


1
“Pick” 的确可以像你说的那样使用。另一个来自你提供的功能请求的想法是:重新声明方法(至少在 BetterB 中的 getSomeBasicA(): BetterA;),这也可以实现(冗长但简单)。我还不确定在实践中该怎么做,但多亏了你,我现在肯定可以让 TypeScript 编译器满意了。谢谢你。 - jonas

3
仅仅因为在Java上能够运行,并不意味着在TypeScript上也能。这两者是非常不同的。首先,TypeScript 依赖于鸭子类型,而 Java 则不是。
如果你想要结合多个接口,最好使用映射类型,而不是经常使用扩展。
我在 medium 上写了一篇关于此的文章,点击这里查看,它应该会有所帮助。

1
"too" -> "两个" - Andreas
1
映射类型组合的可能性似乎很有趣。我肯定没有充分利用它们。感谢您分享您的知识。顺便说一句,我不想让TypeScript成为Java的克隆。我会尽我所知以最符合TypeScript的方式使用它。 - jonas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接