Skip to content

Instantly share code, notes, and snippets.

@notmike101
Created September 18, 2025 22:44
Show Gist options
  • Select an option

  • Save notmike101/a79216e5d66dce72e200e2f7bbde5da9 to your computer and use it in GitHub Desktop.

Select an option

Save notmike101/a79216e5d66dce72e200e2f7bbde5da9 to your computer and use it in GitHub Desktop.
/**
* @typedef {(...args: Parameters<T[M]>) => ReturnType<T[M]>} PatchFunction
* @template {object} T
* @template {keyof T} M
*/
/**
* @typedef {(PatchFunction<T, M>) & {
* readonly original: T[M];
* readonly isPatched: true;
* readonly removePatch: () => void;
* }} PatchReturn
* @template {object} T
* @template {keyof T} M
*/
/**
* Monkey-patch a method on an object or class (prototype or instance).
*
* The returned function is callable like th eoriginal and also exposes:
* - `original` - Reference to the unpatched function
* - `isPatched` - `true`
* - `removePatch()` - Restores the original method (descriptor-safe)
*
* @throws {ReferenceError} If `target`, `method`, or `patchFunction` is not provided.
* @throws {TypeError} If `target` is not an object/function, `method` is not a string, `targetClass.targetMethod` does not exist, or `patchFunction` is not a function.
* @throws {Error} If the target method is already patched or is non-writable/non-configurable.
*
* @example // Patch a method in common object
* Math.abs(-1); // 1
* monkeyPatch(Math, 'abs', (x) => x);
* Math.abs(-1); // -1
* Math.abs.original(-1); // 1
* Math.abs.removePatch();
* Math.abs(-1); // 1
*
* @example // Patch a prototype
* class Counter { inc(x) { return x + 1; }}
* const counter = new Counter();
* counter.inc(1); // 2
* monkeyPatch(Counter.prototype, 'inc', (x = 1) => x + 2);
* counter.inc(1); // 3
* counter.inc.original(1); // 2
* counter.inc.removePatch()
* counter.inc(1); // 2
*
* @template T
* @template {keyof T} M
*
* @param {T} targetClass - Object or prototype that contains the method to patch
* @param {M} targetMethod - Name of the method to patch. Must exist on `targetClass` (own or inherited).
* @param {PatchFunction<T, M>} patchFunction - The patch implementation
*
* @returns {PatchReturn<T, M>} The patched function. Invocable like the original, plus `original`, `isPatched`, and `removePatch`.
*/
export const monkeyPatch = (targetClass, targetMethod, patchFunction) => {
if (!targetClass) {
throw new ReferenceError('Target class must be provided');
} else if (typeof targetClass !== 'object') {
throw new TypeError(`Target class must be of type "object", got type ${typeof targetClass}`);
}
if (!targetMethod) {
throw new ReferenceError('Target method name must be provided');
} else if (typeof targetMethod !== 'string') {
throw new TypeError(`Target method name must be of type "string", got type ${typeof targetMethod}`);
} else if (!(targetMethod in targetClass)) {
throw new ReferenceError(`Target method ${targetMethod} does not exist in target class`);
}
if (!patchFunction) {
throw new ReferenceError('Patch function must be provided');
} else if (typeof patchFunction !== 'function') {
throw new TypeError(`Patch function must be of type "function", got type ${typeof patchFunction}`);
}
if (targetClass[targetMethod].isPatched === true) {
throw new Error(`Function ${targetMethod} already patched. Call ${targetMethod}.revoke first`);
}
const ownDescription = Object.getOwnPropertyDescriptor(targetClass, targetMethod);
if (ownDescription?.writable !== true && ownDescription.configurable !== true) {
throw new Error(`Method ${targetMethod} is non-writable and non-configurable`);
}
const originalFunction = targetClass[targetMethod];
/**
* Wrapper for the patch function
*
* @param {Parameters<T[M]>} args - Arguments passed to the targeted method
* @returns {ReturnType<typeof patchFunction>} Return type of patch function
*/
const wrapper = function (...args) {
return patchFunction.apply(this, args);
};
Object.defineProperties(wrapper, {
original: {
/** @type {T[M]} */
value: originalFunction,
configurable: false,
enumerable: false,
writable: false,
},
isPatched: {
/** @type {boolean} */
value: true,
configurable: false,
enumerable: false,
writable: false,
},
removePatch: {
/**
* Remove the patch
*/
value: () => {
Object.defineProperty(targetClass, targetMethod, {
...ownDescription,
value: originalFunction,
});
},
configurable: false,
enumerable: false,
writable: false,
},
});
Object.defineProperty(targetClass, targetMethod, {
...ownDescription,
value: wrapper,
});
return wrapper;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment