typescriptstatic-methodsabstract-methods

In TypeScript, how can I require a subclass to implement static method?


In TypeScript, I want a given subclass to implement a static method which would be called by the superclass. This method returns data that is really a property of the class and not of an instance and thus makes sense as static rather than non-static from a modeling point of view.

What I'm currently doing in the superclass to call the static method defined in the subclass is this:

const r = Object.getPrototypeOf(this).constructor.staticMethod()

First question: is there a better way to do this? In particular, constructor returns any, which I never like.

Second: how can I ensure at compile-time that the subclass will implement the static method staticMethod? I've tried various approaches, including stuff with satisfies like

type SuperclassStatic = { someMethod(): number }
const Subclass = class extends Superclass { ... } satisfies SuperclassStatic

… but not only does this seem to be invalid syntax in TypeScript 4.9.5, it is also not great that I'd have to remember to add the satisfies SuperclassStatic part in every subclass declaration.

Any better way?


Solution

  • First question: is there a better way to do this?

    If this is within an instance method of the superclass (which you've confirmed in a comment it is), then while you don't have to use Object.getPrototypeOf, you are sadly stuck with this.constructor, which as you say isn't ideal as its type is just Function:

    const ctor = this.constructor as /*...appropriate type, see below...*/;
    // Belt-and-braces runtime check
    if (!ctor.staticMethod) { // Slightly surprising TypeScript doesn't say this will never be true, but handy
        throw new Error(`Invalid subclass, has no staticMethod static method`);
    }
    ctor.staticMethod();
    

    (Note: The !ctor.staticMethod check above only makes sense in conjunction with another change I make below [making the parent class use a different name for the static method than subclasses]. If you don't make that change, you might do if (ctor.staticMethod === Parent.staticMethod) { instead for the condition of that if that will throw on an invalid subclass.)

    how can I ensure at compile-time that the subclass will implement the static method staticMethod?

    I don't think you can if the names are the same, but if you make the names different it's possible with the help of a do-nothing function so we can use a generic to do the check (here I've made the superclass method's name parentStaticMethod, with the subclasses expected to implement staticMethod):

    type Constructor = new (...args: any[]) => any;
    type RequiredSubclassMethods = { staticMethod: (...args: any[]) => any };
    
    class Parent {
        static parentStaticMethod() {
            console.log("Parent.parentStaticMethod()");
        }
    
        method() {
            const ctor = this.constructor as Constructor & RequiredSubclassMethods;
            // Belt-and-braces runtime check
            if (!ctor.staticMethod) { // Slightly surprising TypeScript doesn't say this will never be true, but handy
                throw new Error(`Invalid subclass, has no staticMethod static method`);
            }
            ctor.staticMethod();
        }
    
        static validSub<Cls extends Constructor & RequiredSubclassMethods>(cls: Cls): Cls {
            return cls;
        }
    }
    
    // Works
    const GoodSub = Parent.validSub(
        class GoodSub extends Parent {
            static staticMethod() {
                console.log("GoodSub.staticMethod()");
                super.parentStaticMethod();
            }
        }
    );
    
    // Fails, doesn't have `staticMethod`
    const BadSub = Parent.validSub(
        class BadSub extends Parent {
        }
    );
    
    // Calls `GoodSub.staticMethod`()` via `Parent.prototype.method`
    new GoodSub().method();
    // Fails, doesn't have `staticMethod`
    const BadSub = Parent.validSub(
        class BadSub extends Parent {
        //    ^^^^^^ Argument of type 'typeof BadSub' is not assignable to parameter of type 'Constructor & RequiredSubclassMethods'.
        //           Property 'staticMethod' is missing in type 'typeof BadSub' but required in type 'RequiredSubclassMethods'.(2345)
        }
    );
    

    Playground link

    validSub doesn't have to be a method on Parent, but it seemed a reasonable place to put it. It could be a standalone function instead.

    Of course, using that Parent.validSub is a pain and easily forgotten. :-| I thought we might be able to use implements instead of a function (similar to this answer), but if so, I can't get it to work, and in any case it would also (as you say) be a pain and easily forgotten.