typescriptentity-component-system

How to filter an array based on type parameters typeof


I want to write a simple entity component system.

class Component {
}


class Entity {

  readonly components: Array<Component> = []

}

class T extends Component {}
class F extends Component {
    get hello() { return 'world' }
}

class World {

  readonly entities: Array<Entity> = []
  readonly components: Map<typeof Component, Array<Component>> = new Map()

  first_entity(T: typeof Entity) {
    return this.entities.find(_ => _ instanceof T)
  }

  all_entities(T: typeof Entity) {
    return this.entities.filter(_ => _ instanceof T)
  }

  first<T extends Component>(ctor: { new(...args: any[]): T }) {
    return this.components.get(ctor)
  }

  constructor() {

    this.components.set(T, [new T()])
    this.components.set(F, [new F(), new F()])

    console.log(this.first<F>(F))

  }
}

let world = new World()

I want to do this:

world.first<FComponent>() 
// returns the first instance of an FComponent 
// that extends Component.

of course there are instances of bunch of different components, so I want to keep them in a map with a key typeof SubComponent.

Filter on generic types

Using a generic type argument with `typeof T`

I've seen these answers and came up with the above code, but it only works if I have to type FComponent twice like this:

world.first<F>(F)

where I want this:

world.first<FComponent>() // returns the first instance of an FComponent that extends Component.

Edit:

So what I actually want is world.first() to return something of type FComponent so I can dispatch methods on that specific class.

This signature: first<T extends Component>(ctor: { new(...args: any[]): T }): T

This should type correct: world.first<F>()?.hello

This shouldn't compile:

world.first<T>()?.hello


Solution

  • If you extract the signature into an overload, then it won't get in the way of the implementation. Even though the definition of first has the signature (ctor: typeof Component) => T, the external signature is <T extends Component>(ctor: { new (...args: any[]): T }) => T | undefined.

      first<T extends Component>(ctor: { new(...args: any[]): T }): T | undefined;
      first(ctor: typeof Component) {
        return (this.components.get(ctor) ?? [])[0] // small change to impl
      }
    

    Then you'll be able to use it:

    new World().first(F)?.hello // OK
    //          ^? inferred as <F>(ctor: typeof F) => F | undefined
    

    Playground