Last active
July 17, 2024 08:47
-
-
Save nberlette/1299803c7999246ba150793b33a146e1 to your computer and use it in GitHub Desktop.
recursive method binding tool (typescript)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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