typescriptgenericsentityentity-component-system

Typescript function to accept array or single instance of constructor and return list


EDIT: reproducible link to typescript's playground

I also found a solution that is in the link as well, though I don't actually understand why my first one doesn't work if anyone still wants to educate me.

TLDR;

this does not work

getEntitiesByComponent<T extends Component<any>>(compType: new (...args: any[]) => T | Array<new (...args: any[]) => T>): IEntity[] {

this works

getEntitiesByComponent<T extends new (...args: any[]) => Component<any>>(compType: T | Array<T>): IEntity[]


EDIT 2

Code from reproducible link

export interface IComponent {
  setEntity: (value: IEntity) => void;
  getEntity: () => IEntity;
}

export interface IEntity {
    components: any[];
}

export const entitySet = new Set<IEntity>();

export class Component<T extends object> implements IComponent {
  private _entity: IEntity | undefined;
  
  setEntity(value: IEntity) {
    if (this._entity === value) {
      return;
    }
    
    this._entity = value;
  }
  
  getEntity(): IEntity {
    return this._entity!;
  }
  
  props: T;
  
  constructor(props?: T) {
    this.props = {
      ...props as T
    }
  }
}

export class ViewComponent extends Component<any> {
  constructor(props: any) {
    super(props);
  }
}

export class TransformComponent extends Component<any> {
  constructor(props?: any) {
    super();
  }
}

// THIS TYPING DOESN'T WORK
function getEntitiesByComponent<T extends Component<any>>(compType: new (...args: any[]) => T | Array<new (...args: any[]) => T>): IEntity[] {
  const compTypes = Array.isArray(compType) ? compType : [compType];
  return Array.from(entitySet.values())
    .filter(e => {
      return e.components.some(c => compTypes.some(compType => c instanceof compType))
    });
}

// THIS TYPING DOES WORK
function getEntitiesByComponentThatWorks<T extends new (...args: any[]) => Component<any>>(compType: T | Array<T>): IEntity[] {
    const compTypes = Array.isArray(compType) ? compType : [compType];
    return Array.from(entitySet.values())
        .filter(e => e.components.some(c => compTypes.some(compType => c instanceof compType)))
}

// error
const viewComponentEntities = getEntitiesByComponent([ViewComponent, TransformComponent]);

// no error
const viewComponentEntitiesWorks = getEntitiesByComponentThatWorks([ViewComponent, TransformComponent]);

ORIGINAL

I'm trying to define a function that accepts single instance of- or an array of instances of objects that subclass a single parent class.

// EntityManager.ts

/**
 * this func should receive either a single instance of a constructor that returns a
 * Component<any> or an array of the same and should return only entities that have a
 * component that is an instance of one of them.
**/
getEntitiesByComponent<T extends Component<any>>(compType: new (...args: any[]) => T | Array<new (...args: any[]) => T>): IEntity[] {
  const compTypes = Array.isArray(compType) ? compType : [compType];
  return Array.from(this._entitySet.values())
    .filter(e => {
      return e.components.some(c => compTypes.some(compType => c instanceof compType))
    });
}

ViewComponent and TransformComponent both extend Component.

// ViewComponent.ts

import { Component } from '../components/component';

export type ViewComponentProps = {
  alias: string;
  src: string;
}

export class ViewComponent extends Component<ViewComponentProps> {
  constructor(props: ViewComponentProps) {
    super(props);
  }
}
// TransformComponent.ts
import { Component } from './component';

export type TransformComponentProps = {
  x: number;
  y: number;
  scaleX?: number;
  scaleY?: number;
  zIndex?: number;
}

export class TransformComponent extends Component<TransformComponentProps> {
  private static _defaultProps: TransformComponentProps = {
    x: 0,
    y: 0,
    scaleX: 1,
    scaleY: 1,
    zIndex: 0,
  };
  
  constructor(props?: TransformComponentProps) {
    super({
      ...TransformComponent._defaultProps,
      ...props
    });
  }
}

When I invoke it like so

const viewComponentEntities = EntityManager.getInstance().getEntitiesByComponent([ViewComponent, TransformComponent]);

Typescript gives me this error

TS2345: Argument of type
(typeof TransformComponent | typeof ViewComponent)[]
is not assignable to parameter of type
new (...args: any[]) => Component<any> | (new (...args: any[]) => Component<any>)[]
Type
(typeof TransformComponent | typeof ViewComponent)[]
provides no match for the signature
new (...args: any[]): Component<any> | (new (...args: any[]) => Component<any>)[]

I'm not understanding why I'm getting the error or how to prevent it.


Solution

  • With @jcalz help, it was pointed out I simply had a syntax error.

    getEntitiesByComponent<T extends Component<any>>(compType: new (...args: any[]) => T | Array<new (...args: any[]) => T>): IEntity[]

    My type was looking for a constructor that returned T or Array<new (...args: any[]) => T>

    rather than a single constructor (new (...args: any[]) => T) or array of them Array<new (...args: any[]) => T>

    so this now works

    getEntitiesByComponent<T extends Component<any>>(compType: (new (...args: any[]) => T) | Array<new (...args: any[]) => T>): IEntity[]