diff --git a/src/utils/functions/callable.ts b/src/utils/functions/callable.ts new file mode 100644 index 0000000..a079002 --- /dev/null +++ b/src/utils/functions/callable.ts @@ -0,0 +1,47 @@ +/** + * A symbol representing the `call` function of a {@link Callable} object. + */ +export const CALL = Symbol.for("call"); + +/** + * Represents an object, which can be converted into a {@link Callable} one. + */ +interface SemiCallable { + /** + * A method that should be invoked, when an object is used as a function. + */ + [CALL](...args: unknown[]): unknown; +} + +/** + * Represents an object, which can be called like a function. + * + * @template T - The type of the underlying object. + */ +export type Callable = T & { + /** + * Redirects a call to the {@link CALL} function. + */ + (...args: Parameters): ReturnType; +}; + +/** + * Makes an object callable. + * + * @template T - The type of the object. + * @param obj - The object to make callable. + * + * @returns A new {@link Callable} object with the same properties as the original one, but which can be called like a function. + */ +export function makeCallable(obj: T): Callable { + /** + * Redirects a call to the {@link CALL} function. + */ + function call(...args: unknown[]): unknown { + return (call as unknown as T)[CALL](...args); + } + + Object.assign(call, obj); + Object.setPrototypeOf(call, Object.getPrototypeOf(obj)); + return call as unknown as Callable; +} diff --git a/tests/unit/utils/functions/callable.spec.ts b/tests/unit/utils/functions/callable.spec.ts new file mode 100644 index 0000000..938bf4d --- /dev/null +++ b/tests/unit/utils/functions/callable.spec.ts @@ -0,0 +1,49 @@ +import { CALL, makeCallable } from "@/utils/functions/callable"; + +describe("makeCallable", () => { + test("makes an object callable", () => { + const obj = { + [CALL]: (a: number, b: number) => a + b, + }; + + const callable = makeCallable(obj); + + expect(callable(1, 2)).toBe(3); + }); + + test("preserves object properties", () => { + const obj = { + foo: 42, + + [CALL](): number { + return this.foo; + }, + }; + + const callable = makeCallable(obj); + + expect(callable()).toBe(42); + expect(callable.foo).toBe(42); + }); + + test("preserves object prototype", () => { + class FooClass { + foo: number; + + constructor(foo: number) { + this.foo = foo; + } + + [CALL](): number { + return this.foo; + } + } + + const obj = new FooClass(42); + const callable = makeCallable(obj); + + expect(callable).toBeInstanceOf(FooClass); + expect(callable.foo).toBe(42); + expect(callable()).toBe(42); + }); +});