TypeScript doesn't currently model what happens when a class
's constructor
method returns a value. There is a feature request at microsoft/TypeScript#27594 to support this, but right now TypeScript will only recognize class instance types as having its declared members, and JavaScript doesn't let you declare call signatures for class instances. So even though at runtime the instances of your Callable
subclasses will be callable due to return-overriding and prototype-juggling, TypeScript won't be able to see it.
If you were using TypeScript files instead of JSDoc, I might suggest merging the desired call signature into the Callable
interface, as follows:
class Callable {
constructor() {
var closure: any = function (...args: any) { return closure._call(...args) }
return Object.setPrototypeOf(closure, new.target.prototype)
}
_call(...args: any) {
console.log(this, args)
}
};
interface Callable {
(...arg: any): void;
}
Which you can see at work:
const x = new Callable();
x("a", "b", "c"); // okay
class Pipeline extends Callable {
_call(...args: any) { console.log("overridden"); super._call(...args) }
}
const pipe = new Pipeline();
const result = pipe("data"); // okay
// "overridden"
// [object Function], ["data"]
Playground link to TS code
Unfortunately you can't define interface
s in JSDoc (see feature request at microsoft/TypeScript#33207) and while this might be possible with multiple files or different formats, I'll just forget this approach.
Instead you can use type assertions or the JSDoc equivalent to just tell the compiler that Callable
is of the type
new () => {
(...args: any[]): void,
_call(...args: any[]): void
}
meaning it has a construct signature that returns a callable object with a _call
method. It looks like this:
/** @type {new () => {(...args: any[]): void, _call(...args: any[]): void}}*/
const Callable = /** @type {any} */ (class {
constructor() {
/** @type {any} */
var closure = function (/** @type {any[]} */...args) { return closure._call(...args) }
return Object.setPrototypeOf(closure, new.target.prototype)
}
_call(/** @type {any[]} */...args) {
console.log(this, args)
}
});
And you can verify that it works:
const x = new Callable();
x("a", "b", "c"); // okay
class Pipeline extends Callable {
_call(/** @type {any[]} */ ...args) {
console.log("overridden"); super._call(...args)
}
}
const pipe = new Pipeline();
const result = pipe("data"); // okay
// "overridden"
// [object Function], ["data"]
Playground link to JSDoc-annotated JS code
So that's one way to do it that works for JSDoc. I'm not going to comment much on whether or not the general goal of making TypeScript support callable class instances is worth pursuing, other than to say that such things can lead to surprising edge-case behavior that most JavaScript/TypeScript users will not expect. Obviously use cases will drive such a decision, but it should be made carefully nonetheless.