0

I have a simple system where I generate classes by inheriting them from separate base classes and then mixing in another class to each of them. Here is my mixin class:

type Constructor<T = {}> = new (...args: any[]) => T;

/**
 * Based on the Mixin idea explained here:
 * https://mariusschulz.com/blog/typescript-2-2-mixin-classes
 *
 * @param base
 * @constructor
 */
export function EntityServices<TBase extends Constructor>(base: TBase) {
  return class extends base {
    private _components = {};

    public addComponent(component: Component) {
      throw new Error('Not implemented');
    }

    public removeComponent(component: Component) {
      throw new Error('Not implemented');
    }
  };
}

This mixin is used in another module to create few classes like so:

class ContainerEntityBase extends Phaser.GameObjects.Container {}
class ImageEntityBase extends Phaser.GameObjects.Image {}
class SpriteEntityBase extends Phaser.GameObjects.Sprite {}
class TextEntityBase extends Phaser.GameObjects.Text {}

export const ContainerEntity = EntityServices(ContainerEntityBase);
export const ImageEntity = EntityServices(ImageEntityBase);
export const SpriteEntity = EntityServices(SpriteEntityBase);
export const TextEntity = EntityServices(TextEntityBase);

// Type definitions have to be exported separately so that they can be used as types elsewhere, not as values
// Same name with values (classes) does not matter since TS stores values and types into separate
// namespaces.
export type ContainerEntity = InstanceType<typeof ContainerEntity>;
export type ImageEntity = InstanceType<typeof ImageEntity>;
export type SpriteEntity = InstanceType<typeof SpriteEntity>;
export type TextEntity = InstanceType<typeof TextEntity>;
export type BlackbirdEntity = ContainerEntity | ImageEntity | SpriteEntity | TextEntity;

As you can see, I have exported both actual created classes and their types with one extra union type BlackBirdEntity. Sometimes I'll use variables which may be any of the generated types, since in these cases these instances are operated on by their common mixed in interface.

Next I have the following simple definition which uses the union type:

import { Component } from '../core/Component';
import { BlackbirdEntity } from '../core/entities';

export interface IEntityDefinition {
  name: string;
  components: Component[];
  type: BlackbirdEntity;
}

And I use it like this to create an object which implements the said interface:

import { SpriteEntity } from '../core/entities';
import { IEntityDefinition } from './EntityDefinition';

const clickableEntity: IEntityDefinition = {
  components: [],
  name: 'Clickable',
  type: SpriteEntity
};

However, this gives me the following error on the IDE with SpriteEntity highlighted:

TS2322: Type '{ new (...args: any[]): EntityServices<typeof SpriteEntityBase>.(Anonymous class); prototype: EntityServices<any>.(Anonymous class); } & typeof SpriteEntityBase' is not assignable to type 'BlackbirdEntity'.   Type '{ new (...args: any[]): EntityServices<typeof SpriteEntityBase>.(Anonymous class); prototype: EntityServices<any>.(Anonymous class); } & typeof SpriteEntityBase' is not assignable to type 'EntityServices<typeof TextEntityBase>.(Anonymous class) & TextEntityBase'.     Type '{ new (...args: any[]): EntityServices<typeof SpriteEntityBase>.(Anonymous class); prototype: EntityServices<any>.(Anonymous class); } & typeof SpriteEntityBase' is missing the following properties from type 'EntityServices<typeof TextEntityBase>.(Anonymous class)': _components, addComponent, removeComponent

Why? And how to fix this? The error seems to suggest that the SpriteEntity is missing properties which are in reality in TextEntity's parent class. So is there any way to tell the compiler this kind of type should be ok, even if their parents' definition differs?

Tumetsu
  • 1,661
  • 2
  • 17
  • 29
  • The *value* `SpriteEntity` does not have *type* `SpriteEntity`. Either you mean `IEntityDefinition['type']` to be `Constructor` or you mean put an *instance* of type `SpriteEntity` as the `type` property of `clickableEntity`. – jcalz Apr 07 '19 at 19:23
  • Yeah, I basically mean to put an instance of the `SpriteEntity` as the `type` property of `clickableEntity`. How do I express that with TS type defs? Or how do I attach the _type_ `SpriteEntity` to the _value_ `SpriteEntity`? – Tumetsu Apr 07 '19 at 20:01
  • To clarify, I want to set the _value_ `SpriteEntity` to the propery `type` of the `clickableEntity`. Rather than _type_ the property should rather be _entityClass_ etc. since I want it to refer into a class which is later instantiated. The `clickableEntity` object is a configuration object for a factory. – Tumetsu Apr 07 '19 at 20:09
  • So you want to use `Constructor` instead of `BlackbirdEntity` in your definition of `IEntityDefinition`. – jcalz Apr 07 '19 at 20:11

1 Answers1

1

Your problem is that IEntityDefinition wanted its type property to be an instance of BlackbirdEntity, not a constructor of one. You might be confused because for a class the constructor value generally shares a name with the instance type, even though they are not the same thing.

Anyway, you've already got a Constructor<T> type alias sitting around, so let's use it:

export interface IEntityDefinition {
  name: string;
  components: Component[];
  type: Constructor<BlackbirdEntity>; // you want a constructor, not an instance here
}

This should make your clickableEntity variable initialization compile without errors.

Hope that helps; good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Ahh, that definitely makes sense! For some reason I didn't get that I'd have to specify the type as the constructor and accidentally used an instance. Thank you! – Tumetsu Apr 08 '19 at 06:21