/** * @source https://github.com/flarum/core/blob/master/js/src/common/extend.js */ /** * Type that returns an array of all keys of a provided object that are of the provided type, * or a subtype of the type. */ declare type KeysOfType = { [Key in keyof Type]-?: Type[Key] extends Match ? Key : never; }; /** * Type that matches one of the keys of an object that is of the provided * type, or a subtype of it. */ declare type KeyOfType = KeysOfType[keyof Type]; /** * Extend an object's method by running its output through a mutating callback * every time it is called. * * The callback accepts the method's return value and should perform any * mutations directly on this value. For this reason, this function will not be * effective on methods, which return scalar values (numbers, strings, booleans). * * Care should be taken to extend the correct object — usually, a class' * prototype will be the desired target of extension, not the class itself. * * @example Example usage of extending one method. * extend(Discussion.prototype, 'badges', function(badges) { * // do something with `badges` * }); * * @example Example usage of extending multiple methods. * extend(IndexPage.prototype, ['oncreate', 'onupdate'], function(vnode) { * // something that needs to be run on creation and update * }); * * @param object The object that owns the method * @param methods The name or names of the method(s) to extend * @param callback A callback which mutates the method's output */ export function extend, K extends KeyOfType>( object: T, methods: K | K[], callback: (this: T, value: ReturnType, ...arguments_: Parameters) => void ) { const allMethods = Array.isArray(methods) ? methods : [methods]; for (const method of allMethods) { const original: Function | undefined = object[method]; object[method] = function (this: T, ...arguments_: Parameters): any { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const value = original ? original.apply(this, arguments_) : undefined; Reflect.apply(callback, this, [value, ...arguments_]); return value; } as T[K]; Object.assign(object[method], original); } } /** * Override an object's method by replacing it with a new function, so that the * new function will be run every time the object's method is called. * * The replacement function accepts the original method as its first argument, * which is like a call to `super`. Any arguments passed to the original method * are also passed to the replacement. * * Care should be taken to extend the correct object – in most cases, a class' * prototype will be the desired target of extension, not the class itself. * * @example Example usage of overriding one method. * override(Discussion.prototype, 'badges', function(original) { * const badges = original(); * // do something with badges * return badges; * }); * * @example Example usage of overriding multiple methods. * extend(Discussion.prototype, ['oncreate', 'onupdate'], function(original, vnode) { * // something that needs to be run on creation and update * }); * * @param object The object that owns the method * @param methods The name or names of the method(s) to override * @param newMethod The method to replace it with */ export function override, K extends KeyOfType>( object: T, methods: K | K[], newMethod: (this: T, orig: T[K], ...arguments_: Parameters) => void ) { const allMethods = Array.isArray(methods) ? methods : [methods]; for (const method of allMethods) { const original: Function = object[method]; object[method] = function (this: T, ...arguments_: Parameters): any { return Reflect.apply(newMethod, this, [original.bind(this), ...arguments_]); } as T[K]; Object.assign(object[method], original); } }