I have the following base interface (Animal) and two implementations (Dog and Cat) where each implementation has its own properties (DogProps and CatProps).
interface Animal<T> {
props: T;
eat(): void;
}
interface DogProps {
color: string;
breed: string;
}
class Dog implements Animal<DogProps> {
constructor(public readonly props: DogProps) {}
eat() {}
}
interface CatProps {
color: string;
lives: number;
}
class Cat implements Animal<CatProps> {
constructor(public readonly props: CatProps) {}
eat() {}
}
Next up, I have a simulator class that can receive an optional initial animal (or set the default one which is a Dog). Users can also use the simulator to change the animal to anything else (e.g. Cat) at any time.
Interface Animal<T> is public, so the user can define its own new implementation, e.g. Bird<BirdProps> and run it through the simulator.
I have issues with how to define the simulator to be able to take any implementation of Animal<T> without knowing about T (properties). I tried with these two but it is not working:
interface SimulatorSettings<T> {
initialAnimal: Animal<T>;
simulationSteps: number;
}
class SimulatorTry1<T> {
private _animal: Animal<T>;
constructor(settings?: Partial<SimulatorSettings<T>>) {
// Issue 1: Type 'Dog | Animal<T>' is not assignable to type 'Animal<T>'
this._animal = settings?.initialAnimal ?? new Dog({ color: 'white', breed: 'samoyed' });
this.doStuff(settings?.simulationSteps ?? 100);
}
get animal(): Animal<T> {
return this._animal;
}
setAnimal(animal: Animal<T>, settings: Omit<SimulatorSettings<T>, 'initialAnimal'>) {
this._animal = animal;
this.doStuff(settings.simulationSteps);
}
private doStuff(steps: number) {}
}
class SimulatorTry2 {
// Issue 1: Unable to set <T> here because T is undefined
private _animal: Animal<any>;
// Issue 2: Unable to set "constructor<T>": Type parameters cannot appear on a constructor declaration.
constructor(settings?: Partial<SimulatorSettings<T>>) {
this._animal = settings?.initialAnimal ?? new Dog({ color: 'white', breed: 'samoyed' });
this.doStuff(settings?.simulationSteps ?? 100);
}
// Issue3: Unable to set "get animal<T>": An accessor cannot have type parameters.
get animal(): Animal<T> {
return this._animal;
}
setAnimal<T>(animal: Animal<T>, settings: Omit<SimulatorSettings<T>, 'initialAnimal'>) {
this._animal = animal;
this.doStuff(settings.simulationSteps);
}
private doStuff(steps: number) {}
}
Here is the link to the Typescript Playground with the full code.
My question is: Is this possible to do (I assume it is) and how to do it without defining that T = DogProps | CatProps because users can create new implementations which should be supported?
interface BirdProps {
wingSpan: number;
}
class Bird implements Animal<BirdProps> {
constructor(public readonly props: BirdProps) {}
eat() {}
}
const simulator = new SimulatorTry1();
// Color exists because the default animal is dog
const color = simulator.animal.props.color;
// Now the animal is bird, so props are BirdProps
simulator.setAnimal(new Bird({ wingSpan: 20 }), { simulationSteps: 10 });
const span = simulator.animal.props.wingSpan;