As stated in this blog post and in this stack overflow post, it is possible to make an object callable as follows:
class Callable extends Function {
constructor() {
super();
var closure = function (...args) { return closure._call(...args) }
return Object.setPrototypeOf(closure, new.target.prototype)
}
_call(...args) {
console.log(this, args)
}
}
However, this causes problems with CSP (Content-Security-Policy), like when running in a Chrome extension. It is possible to remove the extends Function
and super()
calls, but this results in type errors. For my use-case, I unfortunately can't ignore this. If there's a way to override the type errors (e.g., using JSDoc), that could also work.
Example usage should look something like this:
class Pipeline extends Callable {
_call(...args) {
// Override _call method here
}
}
// Create new pipeline
const pipe = new Pipeline();
// Call the pipeline
// NOTE: This is where the type error happens if the class is not inherited from `Function`. Something along the lines of:
// This expression is not callable. Type 'Pipeline' has no call signatures.
const result = pipe("data");
Any help will be greatly appreciated.
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"]
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.