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

Viewed 56

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?

2 Answers

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?

First of all I vote for @VLAZ 'es comments.

Since Pair and Triple both implementing Eq, I believe this is why you dont' have a compilation error.

Also, according @VLAZ'es comment, TS does not support higher kinded types.

SInce you are using F-bounded polymorphism, I believe you should give TS a hint about second argument.

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;
  }
}

// hint is here :D
const eq = <Fst extends Eq<Fst>, Scd extends Eq<Scd> & Fst>(x: Fst, y: Scd): boolean => x.eq(y);

const x = eq(new Pair(1, 2), new Triple(1, 2, 3)); // ok
const y = eq(new Triple(1, 2, 3), new Pair(1, 2)); // error

This is not a full answer but I hope it gives you some clues

Related