Search code examples
typescriptset-intersection

Is intersection of a subtype and its supertype the same as the subtype?


Given the following TypeScript code:

interface A { /* ... */ }
interface B extends A { /* ... */ }

type Mystery = A & B;

Objects of the Mystery type should have all the properties of A and all the properties of B because it is an intersection type. Objects of type B should already have all the properties of A because B extends A.

Given these definitions, is there a difference between type Mystery and type B?


Solution

  • To a first approximation, if B extends A, then A & B and B are the same. The compiler will even consider A & B and B mutually assignable:

    function foo<A, B extends A>(u: B, m: A & B) {
      u = m; // okay
      m = u; // okay
    }
    

    although not identical:

    function foo<A, B extends A>(u: B, m: A & B) {    
      var v: B;
      var v: A & B; // error, not considered identical
    }
    

    In practice there are going to be situations in which the compiler treats A & B and B differently. Some of these are compiler implementation details that are seen as bugs or design limitations; I'd have to go digging for these.

    But one specific place where A & B and B could easily be different has to do with how intersections of call signatures are interpreted as overloaded functions which can be called in multiple ways, whereas extensions of interfaces with call signatures tend to just override the parent interface call signature and can only be called one way. For example:

    interface A { method(p: any): void }
    interface B extends A { method(): void }
    

    This is allowed because functions of fewer parameters are assignable to functions of more parameters. The type B only sees the zero-argument method, so you get the following behavior:

    declare const a: A;
    a.method(0); // okay
    declare const b: B;
    b.method(); // okay
    b.method(0); // error!
    

    Since method() is overridden in B, b.method() with no arguments is an error (even though a zero-arg method is assignable to a multi-arg method, you still cannot intentionally call a function with too many arguments without a warning).

    Compare this to the intersection:

    type Mystery = A & B;
    declare const m: Mystery;
    m.method(""); // okay
    m.method(); // okay
    

    If you inspect m.method, you'll see that it has two overloads:

    // 1/2 method(p: any): void 
    // 2/2 method(): void
    

    and therefore can be called in either of the two ways.


    Playground link to code