Reputation: 502
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?
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
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.
Upvotes: 3
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
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.
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