Search code examples
typescriptembind

How to correctly write a type definition where a method returns a class of a specific type that can be instantiated?


While trying to write type definitions for embind, specifically the extend feature, I encountered a problem I cannot solve myself. What I have so far is this:

export declare class Deletable {
    public delete(): void;
}

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

export declare class Extendable extends Deletable {
    public static extend<T extends Constructor<T>>(name: string, o: unknown): new (...args: ConstructorParameters<T>) => T;
    public static implement<T>(o: object): T;
}

class Lexer extends Extendable {
    public constructor(i?: number) { super(); }
}

const WhiteboxLexerTrampoline = Lexer.extend<Lexer>("Lexer", {
});

Playground code is here. The error is on the line with Lexer.extend<Lexer>():

Type 'Lexer' does not satisfy the constraint 'Constructor'. Type 'Lexer' provides no match for the signature 'new (...args: any[]): Lexer'.(2344)

Obviously I'm missing something related to the constructor parameters. How would this have to be written correctly?

Further information: the class returned from extend is used to derive a class from, to be used in my application, like:

export default class WhiteboxLexer extends WhiteboxLexerTrampoline {
    // actual implementation of the lexer.
}

Solution

  • As so often simplified examples for asking a question do not fully represent the real problem at hand. In this case the Lexer class is an ambient declaration, used for the WASM module generated by embind.

    To use such a class it must be exported from the module (which is an interface) and can only be used once the module is loaded. After that happened the types from the WASM module can be exported.

    To use a class like Lexer you need two declarations:

    let antlr4: ANTLR4Wasm;
    try {
        antlr4 = await Module();
    } catch (e) {
        throw new Error("Could not initialize the ANTLR4 runtime:\n" + e);
    }
    
    type Lexer = InstanceType<typeof antlr4.Lexer>;
    const Lexer = antlr4.Lexer;
    

    This can be used to export a single identifier:

    export { Lexer }:
    

    With that in place you can use just Lexer as type or as value. TS will take care automatically. This way you can do both:

    const WhiteboxLexerTrampoline = Lexer.extend<Lexer>("Lexer", {
    });
    

    (WhiteboxLexerTrampoline is of type Lexer, not typeof Lexer or () => Lexer)

    and

    const lexer = new Lexer();
    

    For completeness: the Extendable class has been changed for better type safety:

    export declare class Extendable extends Deletable {
        public static extend<T>(name: string, o: { [key in keyof T]?: T[key]; }): Constructor<T>;
    }
    

    which in turn allows to derive from WhiteboxLexerTrampoline or create an instance of it.