Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active July 17, 2024 08:47
Show Gist options
  • Save nberlette/1299803c7999246ba150793b33a146e1 to your computer and use it in GitHub Desktop.
Save nberlette/1299803c7999246ba150793b33a146e1 to your computer and use it in GitHub Desktop.
recursive method binding tool (typescript)
/**
* Binds all functions and accessors of an object to a specified 'thisArg'.
*
* This has proven to be particularly useful in scenarios where you want to
* subclass the native `Function` API, and want to be able to call `this` from
* within the function body string that you pass to `super()`. This allows you
* to effortlessly create a callable object, which can be invoked directly as
* a function or also by calling the corresponding method that is aliased by
* the super's function body. See the example below for a real-world
* demonstration.
*
* It also covers up the fact that the function was ever bound in the first
* place, since it restores the original name and length properties of the
* function before it was bound.
*
* @param target The object to bind.
* @param thisArg The object to bind to.
* @param excluded Keys to exclude from binding, leaving them exactly as they
* were in the original. Note that any nested keys matching the excluded keys
* will also be affected by this, it is not limited to the top level.
*
* @example
* ```ts
* import { selfBind } from "./self-bind.ts";
*
* export interface Template<O, I = unknown> {
* (input: I): O;
* }
* export class Template<O, I = unknown> extends Function {
* constructor(bound?: boolean) {
* // normally, the call to `this.format` would throw an
* // error, since it is not accessible in the execution
* // context of this eval'd function string.
* super("input", "return this.format(input);");
*
* // the `bound` parameter is just to demonstrate the
* // difference in behavior with / without the binding
* if (bound) return selfBind(this, this);
* }
*
* format(input: I): O {
* return String(input) as unknown as O;
* }
* }
*
* const t = new Template<string, number>();
* t.format(42); // => "42" (works as expected)
* t(42); // (?!)
* // Uncaught TypeError: this.format is not a function
* // at eval (eval at Template (:5:5), <anonymous>:3:13)
* // at <anonymous>:1:22
*
* // now, let's bind everything to the instance
* const t2 = new Template<string, number>(
* true, // bind all methods and accessors to the instance
* );
* t2.format(42); // => "42" (works as expected)
* t2(42); // => "42" (works as expected)
* ```
*/
// deno-lint-ignore ban-types
export function selfBind<T, V extends Function, K extends PropertyKey>(
target: V,
thisArg: T,
...excluded: K[] | [K[]]
): V {
let bound = target;
if (typeof target !== "function") {
if (target == null || typeof target !== "object") return target;
} else {
bound = target.bind(thisArg);
}
return Object.defineProperties(
bound,
Reflect.ownKeys(target).reduce((acc, key) => {
// keys in 'excluded' will be left as-is (unbound)
const exclude = excluded.flat() as PropertyKey[];
if (!exclude.length || !exclude.includes(key)) {
const desc = Object.getOwnPropertyDescriptor(target, key);
if (desc) {
acc[key] = desc;
// recursively bind nested functions / accessors if needed
if (typeof desc.value === "function") {
acc[key].value = selfBind(desc.value, thisArg, ...excluded);
} else {
if (typeof desc.get === "function") {
acc[key].get = selfBind(desc.get, thisArg, ...excluded);
}
if (typeof desc.set === "function") {
acc[key].set = selfBind(desc.set, thisArg, ...excluded);
}
}
}
}
return acc;
}, Object.create(null)),
);
}
selfBind.withToString = withToString;
/**
* Binds all functions and accessors of an object to a specified 'thisArg',
* except for the `toString` method, which will be bound to the original
* target instead.
*
* This is useful when you want to bind all methods and accessors of an object
* to a specific `thisArg`, but want to keep the original `toString` method
* intact. This is particularly useful when you want to subclass the native
* `Function` API, and want to be able to call `this` from within the function
* body string that you pass to `super()` without having to worry about the
* binding.
*/
// deno-lint-ignore ban-types
function withToString<T, V extends Function, K extends PropertyKey>(
target: V,
thisArg: T,
...excluded: K[] | [K[]]
): V {
const toString = selfBind(Function.prototype.toString, target);
const bound = selfBind(target, thisArg, ...excluded);
return new Proxy(bound, {
get(target, prop, receiver) {
if (prop === "toString" && !Object.hasOwn(target, "toString")) {
return toString;
}
return Reflect.get(target, prop, receiver);
},
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment