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;
sim.animal.props.coloris astringat some point, then you can't have it become something other than astring(likeundefined) after a call tosim.setAnimal(new Bird({wingspan:123})). See ms/TS#41339 for a suggestion to support this sort of thing. This means the whole point withTis moot. Do you want me to write up an answer saying this is impossible and why? Or am I missing something about the question?T(used as theprops). I've lost so many hours on this trying to make it work :D Regarding the answer, please do. I will mark it as an accepted answer!any, but I wanted to somehow useTso I can have support from Typescript whenever I typesimulator.animal.propsto show me what is valid there instead ofany.