@lopatnov/callable
v3.1.0
Published
Make TypeScript/JavaScript class instances callable as functions. Four implementation strategies: bind, closure, proxy, and callee.
Maintainers
Readme
@lopatnov/callable
A TypeScript abstract base class that lets you create class instances that behave as callable functions. Extend
Callable<TResult>, implement_call, and everynewinstance becomes directly invokable — with full prototype chain, type safety, and IDE completion preserved.
Table of Contents
Installation
npm install @lopatnov/callableBrowser (CDN via jsDelivr):
<script src="https://cdn.jsdelivr.net/npm/@lopatnov/callable/dist/byBind.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@lopatnov/callable/dist/byCallee.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@lopatnov/callable/dist/byClosure.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@lopatnov/callable/dist/byProxy.umd.min.js"></script>Usage
TypeScript / ES Modules
import CallableByBind from "@lopatnov/callable/byBind";
import CallableByCallee from "@lopatnov/callable/byCallee";
import CallableByClosure from "@lopatnov/callable/byClosure";
import CallableByProxy from "@lopatnov/callable/byProxy";
class Greeter extends CallableByBind<string> {
_call(...args: any[]): string {
return `Hello, ${args[0]}!`;
}
}
const greet = new Greeter(); // instance is a callable function
console.log(greet("World")); // "Hello, World!"CommonJS
const CallableByBind = require("@lopatnov/callable/byBind");
class Greeter extends CallableByBind {
_call(...args) {
return `Hello, ${args[0]}!`;
}
}
const greet = new Greeter();
console.log(greet("World")); // "Hello, World!"Browser UMD
After loading the script tag, the class is available as the global callable:
<script src="https://cdn.jsdelivr.net/npm/@lopatnov/callable/dist/byBind.umd.min.js"></script>
<script>
class Greeter extends callable {
_call(...args) {
return `Hello, ${args[0]}!`;
}
}
const greet = new Greeter();
console.log(greet("World")); // "Hello, World!"
</script>API
Abstract method
Every subclass must implement:
abstract _call(...args: any[]): TResult| Parameter | Type | Description |
| --------- | ------- | ---------------------------------------------------------- |
| ...args | any[] | Arguments passed when the instance is called as a function |
Returns: TResult — the value returned to the caller.
Implementations comparison
| Class | Import path | Mechanism | Strict mode | Modifies prototype |
| ------------------- | ------------------------------ | --------------------------------- | ------------------------ | ------------------ |
| CallableByBind | @lopatnov/callable/byBind | Function.prototype.bind | ✅ Yes | ❌ No |
| CallableByCallee | @lopatnov/callable/byCallee | arguments.callee | ❌ No (sloppy mode only) | ❌ No |
| CallableByClosure | @lopatnov/callable/byClosure | Closure + Object.setPrototypeOf | ✅ Yes | ✅ Yes |
| CallableByProxy | @lopatnov/callable/byProxy | Proxy apply trap | ✅ Yes | ❌ No |
CallableByBind<TResult>
Uses Function.prototype.bind to return a bound function from the constructor. The bound function delegates to _call on the original instance.
Pros: no deprecated APIs, no prototype modification, works in strict mode.
Cons: constructor returns a bound wrapper, not the raw instance.
import CallableByBind from "@lopatnov/callable/byBind";
class Multiplier extends CallableByBind<number> {
constructor(private factor: number) {
super();
}
_call(value: number): number {
return value * this.factor;
}
}
const triple = new Multiplier(3);
console.log(triple(7)); // 21CallableByCallee<TResult>
Uses arguments.callee inside the dynamically constructed function body.
Pros: minimal implementation.
Cons: arguments.callee is forbidden in strict mode — not suitable for modern bundlers.
const CallableByCallee = require("@lopatnov/callable/byCallee");
class Adder extends CallableByCallee {
_call(a, b) {
return a + b;
}
}
const add = new Adder();
console.log(add(2, 3)); // 5CallableByClosure<TResult>
Creates an anonymous function in the constructor that closes over itself, then rewires the prototype chain with Object.setPrototypeOf.
Pros: works in strict mode, no bound wrapper.
Cons: calls Object.setPrototypeOf, which may affect JIT optimization.
import CallableByClosure from "@lopatnov/callable/byClosure";
class Counter extends CallableByClosure<void> {
private count = 0;
_call(): void {
console.log(++this.count);
}
}
const counter = new Counter();
counter(); // 1
counter(); // 2CallableByProxy<TResult>
Wraps the constructed instance in an ES6 Proxy with an apply trap that delegates to _call.
Pros: clean ES2015+ approach, works in strict mode, no prototype modification.
Cons: adds a Proxy wrapper with a minor per-call overhead.
import CallableByProxy from "@lopatnov/callable/byProxy";
class Logger extends CallableByProxy<void> {
_call(message: string): void {
console.log(`[LOG] ${message}`);
}
}
const log = new Logger();
log("Server started"); // [LOG] Server startedDistribution Formats
Each implementation is published in four formats:
| Format | File | Use case |
| -------------- | ------------------------ | -------------------------------- |
| CommonJS | dist/{name}.cjs | Node.js require() |
| ES Module | dist/{name}.esm.mjs | Bundlers, native ESM import |
| UMD | dist/{name}.umd.js | Browser globals (with sourcemap) |
| UMD (minified) | dist/{name}.umd.min.js | Production browser |
TypeScript declaration files (*.d.ts) are included for all four implementations.
Demo
Try it live:
Contributing
Contributions are welcome! Please read CONTRIBUTING.md before opening a pull request.
- Bug reports → open an issue
- Questions → Discussions
- Found it useful? A star on GitHub helps others discover the project
Built With
- TypeScript — strict typing throughout
- Rollup — bundled to ESM, CJS, and UMD formats
- Ava — fast, concurrent test runner
- OXLint — fast JavaScript/TypeScript linter
- dprint — code formatter
License
Apache-2.0 © 2019–2026 Oleksandr Lopatnov · LinkedIn
