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?
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)
}
);
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.