I have a list of Components in a class Entity. These components extend the interface Component.
class Entity {
...
const components: Component[] = [];
...
}
Where specific components implements the interface Component
class SpecificComponent0 implements Component { ... }
Now I want to query the entity instance e and get a component if it matches the type fed into the query, something like this:
const specificComponent0 = e.getSpecificComponent<SpecificComponentClass0>();
Or perhaps like this
const specificComponent0 = e.getSpecificComponent(instanceof SpecificComponentClass0)
But I can't seem to figure out a way to do it in the entity's get function.
This is a tricky one as you are mixing runtime and build-time concerns. Referring to the examples you suggested:
const specificComponent0 = e.getSpecificComponent<SpecificComponentClass0>();
This definitely isn't going to work, because the angle brackets specify a "Type Parameter", which only exists at build time. Since what you are trying to do involves logic, you need to pass something into the function at runtime to help it pick the correct element.
const specificComponent0 = e.getSpecificComponent(instanceof SpecificComponentClass0)
The return value of the instanceof operator is a boolean value. You are passing either true or false into this function, which isn't very useful.
You have two problems here.
ComponentProblem 1 can be solved by passing in the type Constructor function and then matching it with the constructor property of the instantiated Component
class Entity {
constructor(private components: Component[]) {}
getSpecificComponent(thing: new () => Component): Component | undefined {
return this.components.find(component => component.constructor === thing)
}
}
This works perfectly fine, but your getSpecificComponent function is going to return a value typed as Component | undefined, which isn't very useful if you want to use properties that only exist on one of the specific types.
To solve Problem 2 (without casting the return value, which you really shouldn't do), we need to
true, it can safely narrow the type down to the generic typeclass Component {}
class OtherThing1 extends Component { name = 'thing1' }
class OtherThing2 extends Component { name = 'thing2' }
class OtherThing3 extends Component { name = 'thing3' }
const getNarrower = <T extends Component>(thingConstructor: new () => T) =>
(thing: Component): thing is T => thing.constructor === thingConstructor
class Entity {
constructor(private components: Component[]) {}
getSpecificComponent<T extends Component>(thing: new () => T): T | undefined {
return this.components.find(getNarrower(thing))
}
}
const e = new Entity([new OtherThing1(), new OtherThing2()])
const thing = e.getSpecificComponent(OtherThing1)
console.log(thing) // [LOG]: OtherThing1: { "name": "thing1" }
const thingNotHere = e.getSpecificComponent(OtherThing3)
console.log(thingNotHere) // [LOG]: undefined