2

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.

Xenova
  • 330
  • 5
  • 10
  • 2
    `const fn = () => {};` There's your callable object. You can use `fn()` to call the function, and `fn.property = 42` to modify the object. – kelsny Apr 21 '23 at 13:59
  • 1
    This might well be an [XY problem](https://en.wikipedia.org/wiki/XY_problem), since "callable class instances" aren't really a conventional approach to any issue I've heard about; can you describe your underlying use case so that people can either tell you a better approach or say "yeah, callable class instances are the way to go here" and then people might be more motivated to find something that works well enough. – jcalz Apr 21 '23 at 14:10
  • @jcalz It's mainly a design decision that is needed to ensure compatibility with an existing python library. In python, it's simple to override the `__call__` dunder method, however, the same functionality isn't built in to javascript. See [here](https://github.com/xenova/transformers.js) for the project it will be implemented in. – Xenova Apr 21 '23 at 15:00
  • Right, but JavaScript/TypeScript isn't python, so some changes will be needed no matter what; I still haven't seen why you'd need a *class* with callable instances, as opposed to some factory function with callable outputs. Could you [edit] the question to show motivating code examples so we can discuss approaches? For example, would `const f = newCallable(⋯); f(⋯)` work for you instead of `const f = new Callable(⋯); f(⋯)`? If not, please show why. (Note that a link to an external project is a nice supplement but it doesn't suffice unless your question is *about* that project.) – jcalz Apr 21 '23 at 15:07
  • 1
    If all you're looking for is to suppress TS errors you can do that with enough type assertions, as shown [in this playground link](https://tsplay.dev/m0zXRw); I really wouldn't recommend something like this without a strong use case, since it could lead to some weird/surprising behavior, but if that meets your needs I could write up an answer explaining it (with caveats, though). What do you think? – jcalz Apr 21 '23 at 15:15
  • @jcalz I've updated the question to show example usage. Regarding suppressing the errors, if you can do it with JSDoc (since I am using that to automatically generate .d.ts files), that would be perfect! All I really care about is that it does not give a warning that says "`Pipeline` has no call signature" when someone tries to use it. – Xenova Apr 21 '23 at 15:28
  • 2
    You should really tag and/or [edit] the question to mention JSDoc since that will definitely change the form of any solution. I'm no JSDoc expert but I think [this version](https://tsplay.dev/w2204w) is the same as what I wrote above but in JSDoc, more or less. With your `Pipeline` bit added. Does that meet your needs and I should write an answer? Or is something missing? – jcalz Apr 21 '23 at 16:47
  • Woah - that looks like it works!!! Yes please - you can write an answer for it. I will do some more testing to make 100% sure everything works :) Thanks! – Xenova Apr 21 '23 at 17:13

1 Answers1

1

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 interfaces 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.

jcalz
  • 264,269
  • 27
  • 359
  • 360