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?
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:
x
, y
, z
private and provide gettersSee Can I force the TypeScript compiler to use nominal typing?