Search code examples
typescriptgenericsinterfacetypeerrortype-safety

Why doesn't the following TypeScript program throw a type error?


Consider the following program.

interface Eq<A> {
  eq(this: A, that: A): boolean;
};

class Pair<A> implements Eq<Pair<A>> {
  constructor(public x: A, public y: A) {}

  eq(this: Pair<A>, that: Pair<A>): boolean {
    return this.x === that.x && this.y === that.y;
  }
}

class Triple<A> implements Eq<Triple<A>> {
  constructor(public x: A, public y: A, public z: A) {}

  eq(this: Triple<A>, that: Triple<A>): boolean {
    return this.x === that.x && this.y === that.y && this.z === that.z;
  }
}

const eq = <A extends Eq<A>>(x: A, y: A): boolean => x.eq(y);

console.log(eq(new Pair(1, 2), new Triple(1, 2, 3)));
console.log(eq(new Triple(1, 2, 3), new Pair(1, 2)));

I would have expected the TypeScript compiler to complain about the last two lines, because you shouldn't be able to apply the eq function to two values of different types. However, the TypeScript compiler doesn't throw any type error for the above program. The result of the above program is true and false.

Why doesn't the TypeScript compiler throw a type error for the above program? How can we get it to correctly catch these kinds of type errors?


Solution

  • The program compiles due to structural subtyping used in TypeScript (as opposed to nominal subtyping often present in other programming languages).

    Note that your Triple class is a can be assigned to a variable of type Pair:

    const p: Pair<number> = new Triple(1, 2, 3);
    

    In your example:

    console.log(eq(new Pair(1, 2), new Triple(1, 2, 3)));
    console.log(eq(new Triple(1, 2, 3), new Pair(1, 2)));
    

    The type of eq is inferred to be:

    const eq: <Pair<number>>(x: Pair<number>, y: Pair<number>) => boolean
    

    As shown above, Triple is a valid argument for a parameter of type Pair, so everything compiles cleanly.

    You could add a different private field to your classes to simulate nominal subtyping. In this particular example, you could pick between one of two options:

    • add additional marker field
    • make x, y, z private and provide getters

    See Can I force the TypeScript compiler to use nominal typing?