Interfaces in TypeScript vs Java

Viewed 1683

This fails in 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.

... while this works in 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 {
    
}

... and I wonder why? Is it not type safe (how could it be broken, why does the Java compiler allow it)? Is there a name for the feature that one compiler supports and the other doesn't?

2 Answers

As you've seen, TypeScript requires that when extending multiple interfaces via the comma notation (e.g., interface X extends Y, Z { ... }), any member names common to more than one of the parent interfaces need to have identical types.

The definition of BetterB runs afoul of this restriction; it cannot be declared to extend both B and BetterA. B has a getBasicA member of type () => A, while BetterA has a getBasicA member of type () => BetterA. Those types are not identical, so you get an error there, which ends up unraveling the whole thing because your interface definitions are recursive.


This requirement that common properties in an extension of multiple interfaces have identical types is perhaps more restrictive than it needs to be. There is an open feature request at microsoft/TypeScript#16936 asking to allow common members to have non-identical but "compatible" types (presumably meaning that the types have some overlap). It's not 100% clear what the behavior should be in this situation; perhaps the new interface's property type would be an intersection of the property types from each parent interface, or maybe some more sophisticated selection process could occur. But in any case, it's just not part of the language as of TS4.1.

To reiterate: it's not that what you're trying to do is not type safe, it's just not supported.


So, what can you do? One notable thing about TypeScript is that the type system is structural and not nominal as in Java. Types are compared by their shape in TypeScript, not by their declarations. In Java, if you can't declare that interface X extends Y, Z, then you can't use an X when you are asked for a Y or a Z. TypeScript has no such restriction: if X's structure is compatible with both Y and Z, you can use an X in place of a Y or a Z even if the declaration of X doesn't mention Y or Z explicitly.

That means if we can figure out what member names and types we'd like to see on BetterB, we can just define BetterB as having that shape without explicitly saying extends B, BetterA. If we do it right, it will automatically extend those interfaces.

The simplest way I can think of to do this is:

interface BetterB extends Pick<B, "getSomeBetterA">, BetterA { }

Here we are still extending BetterA, but instead of explicitly extending all of B, we are using the Pick utility type to say that we only want to extend the part of B with a getSomeBetterA member. The offending getBasicA method is excluded, so there's no error. And since BetterA's getBasicA method is assignable to B's getBasicA method, BetterB still extends B structurally, as can be seen here:

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

There is no error assigning a value of type BetterB to a variable of type B. You didn't declare that BetterB extends B but it happened anyway.


Playground link to code

Just because it works in Java doesn't mean it should work on Typescript. It's two very different things. For starters Typescript relies on duck typing and Java doesn't.

if you want to combine multiple interfaces it's better to use mapped types instead of extends a lot of times.

I wrote about that on medium here check it out, it should probably help

Related