doppelgreener
doppelgreener

Reputation: 5094

How can I pass abstract class types as a generic parameter in Typescript?

I'm trying to figure out how I can pass abstract class types as a parameter to a generic method in TypeScript.

I'm working on an Entity-Component system in Typescript. This means I have entities, which are containers of components, and I can fetch any arbitrary component from them. Here's a minimal example:

abstract class Component { }
class PlayerController extends Component { }
class MonsterController extends Component { }
class LaserGun extends Component { }
class Collider extends Component { }
class Movement extends Component { } // etc, you get the idea

class Entity {
  public components: Component[] = [];

  public fetch<T extends Component>(componentType: { new(): T }): T | undefined {
    return this.components.find(c => c instanceof componentType) as T;
  }
}

Now I can fetch individual component types like this:

const entity = new Entity();
entity.components.push(new PlayerController());
entity.components.push(new Movement());
entity.components.push(new LaserGun());
const playerController = entity.fetch(PlayerController);
// ^ this variable controller is typed PlayerController,
// so I can use PlayerController-specific features.

screenshot of the ability to do the above fetch

However, I want to upgrade this structure by being able to request abstract types. For example, I could have other Weapons beside LaserGun, and just request any Weapon at all that's attached to the class without caring which kind of weapon it is, like this:

abstract class Weapon extends Component {
  public abstract attack(): void;
}
class LaserGun extends Weapon {
  public attack(): void { /* pew pew */ }
}
class Sword extends Weapon {
  public attack(): void { /* swish swish */ }
}

I can't actually fetch this abstract class though, because abstract classes don't have constructors, and TypeScript doesn't let me pass them as a parameter to the above method:

const weapon = entity.fetch(Weapon);
//                          ~~~~~~
// Argument of type 'typeof Weapon' is not assignable to parameter of type 'new () => Weapon'.
//  Cannot assign an abstract constructor type to a non-abstract constructor type.ts(2345)

screenshot of the error described in the above code block occurring in Visual Studio Code

How can I update my fetch() method to allow me to pass any class type that extends Component, including abstract classes?

For now my workaround is to simply make Weapon a non-abstract class, but I'd like to know how to do this in a way that lets me use abstract classes.

Upvotes: 1

Views: 1523

Answers (1)

Mirco S.
Mirco S.

Reputation: 2610

You can use abstract construct signatures for that (playground):

class Entity {
  public components: Component[] = [];

  public fetch<T extends Component>(componentType: abstract new() => T): T | undefined {
    return this.components.find(c => c instanceof componentType) as T;
  }
}
// no errors
const weapon = entity.fetch(Weapon); 
const sword = entity.fetch(Sword);

Upvotes: 2

Related Questions