Search code examples
typescript

Async construction pattern with generic parent class


How to type the constructWhenReady method in such a way that the return type is simply just Child, without changing the 'calling' code or the Child class. Basically I am just searching for a solution to have an async constructor, but the proposed solutions fall apart when the Parent class is generic.

class Parent<PropType extends any> {
    static isReadyForConstruction = new Promise(r => setTimeout(r, 1000));

    static async constructWhenReady<T extends typeof Parent>(this: T): Promise<InstanceType<T>> {
        await this.isReadyForConstruction;
        return new this() as InstanceType<T>;
    }

    get something(): PropType {
       // ...
    }
}

class Child extends Parent<{ a: 'b' }> { }

// Error: '{ a: "b"; }' is assignable to the constraint of type 
// 'PropType', but 'PropType' could be instantiated with a different 
// subtype of constraint 'unknown'.
const x = await Child.constructWhenReady(); 


Rationale: The Child classes and the code consuming the Child classes will be written by less experienced developers and need to be as clean as possible (it's test automation code). The simplest solution would be do to just require everyone to write their code as await new Child().init(), which is fine, but I would far rather have an error message that pops up whilst they are typing their code rather than during runtime.


Solution

  • The constructWhenReady method doesn't care which type the generic argument PropType is instantiated with. It should explicitly state so:

    class Parent<PropType> {
        static isReadyForConstruction = new Promise(r => setTimeout(r, 1000));
    
        static async constructWhenReady<T extends typeof Parent<unknown>>(this: T): Promise<InstanceType<T>> {
        //                                                     ^^^^^^^^^
            await this.isReadyForConstruction;
            return new this() as InstanceType<T>;
        }
    
        get something(): PropType {
           // ...
        }
    }
    

    Notice that you can even get rid of the as InstanceType<T> cast when you write

        static async constructWhenReady<T extends Parent<unknown>>(this: {new(): T; isReadyForConstruction: Promise<unknown>}): Promise<T> {
            await this.isReadyForConstruction;
            return new this();
        }