I need some help with a litter of abstractions in typescript.
I want to constraints a member to just accept types as values but those types need to implement or extend other types or interfaces.
Eg.
The following example does not represent or is related to a real data model, instead is an example to illustrate the goal.
interface Profession {
work()
}
class Engineer implements Profession {
work() {...}
}
class Doctor {
work() {...}
}
interface ProfessionRankingMap {
top1ProfessionType: // Here is where I don't know how to constraint
top2ProfessionType: // Here is where I don't know how to constraint
}
const classAProfessionRankingMap: ProfessionRankingMap {
// This is okay, Type Engineer implements Profession interface
top1ProfessionType: Engineer
// This is bad, Type Doctor doesn't implement Profession interface
top2ProfessionType: Doctor
}
const classBProfessionRankingMap: ProfessionRankingMap {
// This is bad, [new Engineer()] returns an instance not a type
top1ProfessionType: new Engineer()
// This is bad, [new Doctor()] returns an instance not a type
top2ProfessionType: new Doctor()
}
To express a class in a type, you need a constructor signature:
interface ProfessionRankingMap {
// The implementer must have a constructor with no args that returns Profession
top1ProfessionType: new () => Profession
top2ProfessionType: new () => Profession
}
const classAProfessionRankingMap: ProfessionRankingMap = {
top1ProfessionType: Engineer,
top2ProfessionType: Doctor
}
If we want to accept classes with constructors that have any number of arguments we can use new (... args:any[]) => Profession
.
The second part of your problem is a bit more complicated. Typescript uses structural typing to determine type compatibility. So the structure of the class matters not the implements
clause. The implements
clause will just help give you errors when you declare the class, ensuring that it has all the required members, so you get errors early not when you try to use the class where the interface is expected.
What this means is that as long as Doctor
has all the members of Profession
there is no way to prevent it being passed in where Profession
is expected.
The only solution is to use an abstract class with a private member. The private member will ensure that no other class except those extending the abstract class will be compatible with the abstract class type:
abstract class Profession {
private _notStructural: undefined;
abstract work(): void
}
class Engineer extends Profession {
work() { }
}
class Doctor {
work() { }
}
interface ProfessionRankingMap {
top1ProfessionType: new () => Profession
top2ProfessionType: new () => Profession
}
const classAProfessionRankingMap: ProfessionRankingMap = {
top1ProfessionType: Engineer,
top2ProfessionType: Doctor // error
}