Recognize only concrete subclasses of an abstract class

Viewed 100

I'm trying to create a set that will allow me to add ONLY concrete sub classes of an abstract class. Looking at similar questions here, I was able to reach a point where I see an error whenever I try to add the abstract class or other random classes.

The problem occurs when I "trick" the compiler and implements a class with the same function as my abstract class. TSC can't tell the difference between a class that extends the abstract class and just a class that implements a function:

abstract class Animal {
  walk(){}
}

class Dog extends Animal {
  override walk() {console.log("");}
}

class ServiceDog extends Dog {
  override walk() {console.log("")}
}

class Cat extends Animal {
  override walk() {console.log("");}
}

class Person {
  walk() {console.log("‍♂️");}
}

const map: Map<string, new () => Animal> = new Map();

map.set("dog", Dog); // All good
map.set("animal", Animal); // throws an error
map.set("person", Person); // accepts it <-- HERE I WANT TO GET AN ERROR

If I'll remove the walk() function from class Person it would show Argument of type 'typeof Person' is not assignable to parameter of type 'new () => Animal'. Property 'walk' is missing in type 'Person' but required in type 'Animal'.

Which makes me wonder - Does Typescript only checks classes' inheritance by their properties and functions? Is there a way to enforce it base on the extends word?

Link to playground

Thanks

1 Answers

TypeScript is structurally-typed, including classes.

If you want to prevent this in your example, you can change the structure in a way that can't be duplicated: for example, by using a private field to essentially "brand" the class, like this:

TS Playground

abstract class Animal {
  #type = 'Animal';
  abstract walk(): void;
}

class Dog extends Animal {
  walk() {console.log("");}
}

class Person {
  #type = 'Animal';
  walk() {console.log("‍♂️");}
}

const map: Map<string, new () => Animal> = new Map();

map.set("dog", Dog); // OK
map.set("animal", Animal); // NOK
map.set("person", Person); // NOK
Related