Search code examples
javascripttypescriptentity-component-system

how make a generic method in a abstract parent class for implementation typescript?


What will be the best and (proper) approach to make this in TypeScript. $Foo.getInstance('uid') should return a FooInstance based on implementations?

I want in the abstact class Entity a method to get a instance from a pool, for return the implemented EntityInstance.

abstract class Entity {
    abstract Instance: Partial<typeof EntityInstance>;
    instances: { [uid: string]: EntityInstance } = {};
    getInstance (uid: string ) {
        return this.instances[uid]
    }
}
abstract class EntityInstance {
    prop='';
}

class Foo extends Entity {
    Instance = FooInstance // @implementation
}
class FooInstance extends EntityInstance {

}


const $Foo = new Foo();
// need return InstanceType<FooInstance>
const instance = $Foo.getInstance('uid'); 

So example here: const instance = $Foo.getInstance('uid') should be a FooInstance; But it actually a EntityInstance thats correct ! enter image description here

so i try change this method getInstance to something like this.

    getInstance <t=this>(uid: string ): InstanceType<this['Instance']> {
        return this.instances[uid]
    }

It working !:) but make some error type. Am noob with ts documentations, what change i can made to make this logic work fine. I know ts is powerful, but am not sure how to make this work fine for intelisence in my ide.

enter image description here


minimal reproductive demo typescript i want myInstance.__foo2; not produce error.


Solution

  • You could try changing the typings in Entity to the following:

    abstract class Entity {
        abstract Instance: new () => EntityInstance;
    
        instances: Record<string,
            InstanceType<this["Instance"]> | undefined
        > = {};
    }
    

    The Instance property is a constructor that returns an EntityInstance (or a subtype of it). And the instances property type depends on the type of Instance; by using polymorphic this, we are saying for any subclass of Entity the instances property will depend on the type of Instance in the same subclass.

    This gives you the behavior you're looking for, at least for the example code:

    class Foo extends Entity {
        Instance = FooInstance;
    }
    class FooInstance extends EntityInstance {
        __foo2 = 2;
    }
    
    const $Foo = new Foo();
    $Foo.instances['myUid'] = new $Foo.Instance();
    
    const myInstance = $Foo.instances['myUid'];
    myInstance.__foo2; // okay
    

    Note that polymorphic this types can be a bit of a pain to work with inside the subclasses themselves:

    class Foo extends Entity {
        Instance = FooInstance;
    
        constructor() {
            super();
            this.instances.abc = new FooInstance(); // error!
            this.instances.abc = new this.Instance(); // error!
        }
    
    }
    

    I'm not sure if you need to do something like this, but trying to set a property on this.instances inside Foo fails because the compiler doesn't know what this will be if someone comes along and subclasses Foo. It treats this like an unspecified generic type and is not really able to verify that any particular value is assignable to it. In such cases you might need to use type assertions to suppress errors.


    Another approach is to make Entity a generic class where the type parameter T corresponds to the particular subtype of EntityInstance in subclasses:

    abstract class Entity<T extends EntityInstance> {
        abstract Instance: new () => T;
        instances: Record<string, T | undefined> = {};
    }
    

    Any particular subclass needs to specify what T should be (which is a bit redundant), but then everything works... both inside and outside the subclass:

    class Foo extends Entity<FooInstance> {
        Instance = FooInstance;
        constructor() {
            super();
            this.instances.abc = new FooInstance(); // okay
            this.instances.abc = new this.Instance(); // okay
        }
    }
    
    myInstance.__foo2;  // still okay
    

    Playground link to code