How to prevent unintended type compatibility in TypeScript

Your classes Dog and Snake are structurally equivalent types, so they are assignable to each other as far as Typescript is concerned. The behaviour you want is that of a nominal type system, which Typescript doesn’t have, but you can emulate it by changing the types to be structurally distinct.

To do this, you can add a property named something like __brand, which doesn’t collide with any real property a type might have, and the property can have a distinct type for each class; it can simply be the class itself, or the name of the class as a string literal. To avoid any cost at runtime, you can declare the property without initialising it, so the property doesn’t really exist but Typescript thinks it does. To disable the error for an uninitialised property, you can make the property optional, or use ! instead of ? to lie to the compiler about the property being initialised.

class Dog implements Animal {
  private readonly __brand?: Dog;

  constructor(public amountOfLegs: number) {}
}

class Snake implements Animal {
  private readonly __brand?: Snake;

  amountOfLegs: number;
  constructor() {
    this.amountOfLegs = 0;
  }
}

Then if you try to use a Dog where a Snake is expected, you’ll get a type error because the __brand property has the wrong type.

Playground Link

Leave a Comment