P. Protas
P. Protas

Reputation: 502

How to prevent unintended type compatibility in TypeScript

Let's say we have an interface Animal

interface Animal {
  amountOfLegs: number;
}

And then two different classes implement this interface, Dog and Snake:

class Dog implements Animal {
  constructor(public amountOfLegs: number) {
  }
}

class Snake implements Animal {
  amountOfLegs: number;

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

Then we want to create a function that is specific to the Snake:

function functionSpecificForSnake(snake: Snake): any {
  //Do something with the snake object...
}

But when the function is called with a Dog object, TS doesn't complain about wrong types:

functionSpecificForSnake(new Dog(4)); // No compiler error, but possible runtime error?

Playground Link

So my question is: Can i prevent this behavior? I know that this is called type compatibility, but i don't see a way to prevent it. Using the "strictFunctionTypes": true option in my tsconfig.json doesn't seem to do anything.

Upvotes: 4

Views: 1552

Answers (3)

kaya3
kaya3

Reputation: 51034

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

Upvotes: 3

matt helliwell
matt helliwell

Reputation: 2678

As it stands, no you can't. In the link you referenced: "To check whether y can be assigned to x, the compiler checks each property of x to find a corresponding compatible property in y". So the compiler ignores the 'implements interfaces' bit of the classes and just looks at the shape of each class.

Snakes and dogs are the same shape so can be assigned to one another.

You could try and add a discriminator to each class, something like:

interface Animal {
    disc: string
    amountOfLegs: number;
}

class Dog implements Animal {
    disc = 'Dog' as const
    constructor(public amountOfLegs: number) {
    }
}

class Snake implements Animal {
    disc = 'Snake' as const
    amountOfLegs: number;

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

function functionSpecificForSnake(snake: Snake): any {
    //Do something with the snake object...
}

functionSpecificForSnake(new Dog(4))

This now gives the error

Argument of type 'Dog' is not assignable to parameter of type 'Snake'. Types of property 'disc' are incompatible. Type '"Dog"' is not assignable to type '"Snake"'.(2345)

Upvotes: 0

sunknudsen
sunknudsen

Reputation: 7250

I believe TypeScript checks property types so your problem is both Dog and Snake have the same properties and types... so they are identical.

See TypeScript Playground.

interface Animal {
  amountOfLegs: number;
}
class Dog implements Animal {
  constructor(public amountOfLegs: number) {
  }
}
class Snake implements Animal {
  public amountOfLegs: number;
  public venomous: boolean;
  constructor() {
    this.amountOfLegs = 0;
    this.venomous = true;
  }
}
function functionSpecificForSnake(snake: Snake): void {
  //Do something with the snake object...
}
functionSpecificForSnake(new Dog(4));

Upvotes: 0

Related Questions