Type safe monkey patching with TypeScript
 
            How to wrap any function without using any types to keep your code as strict as possible.
Our goal is to have a universal solution so it should:
- wrap function with zero or more arguments
- preserve types of original function (infer arguments and return type)
- should work under strict compiler options of TypeScript
This will allow us to:
- create wrappers around other functions (e.g. log function results or dispatch analytics events)
- accept other functions as arguments to call them inside
- call other functions before or after target function
- or other cases.
Solution
Look, no any and as SomeType lies to compiler π
const wrap = <F extends (...args: never) => unknown>(f: F): F => f;
// no args
// β
 const f1: () => number
const f1 = wrap(() => 55);
// with args
// β
 const f2: (msg: string, shift: number) => number
const f2 = wrap((msg: string, shift: number) => msg.length + shift);
This solution works with strict compiler options of tsconfig.json
{
  "compilerOptions": {
    "strict": true,
  }
}TypeScript version is 5.5.3 and you can play with this code on TS playground.
How it works
- use generics function to infer types from arguments
- constraint generic argument with extends
- args: neveris a trick here to allow path functions of different signatures with and without arguments
- unknownas return type so we actually don't restrict return type
But, caveats...
Unfortunately, for more complex scenarios you may still need to mark your code with @ts-ignore or @ts-expect-error or @eslint-disable-next-line in cases like this:
const anotherWrap = <F extends (...args: never) => unknown>(f: F): F => {
    // β Error Type '(...args: never) => unknown' is not assignable to type 'F'.
    //   '(...args: never) => unknown' is assignable to the constraint of type 'F', but 'F'
    //   could be instantiated with a different subtype
    //   of constraint '(...args: never) => unknown'.(2322)
    const internalWrap: F = (...args) => f(...args);
    return internalWrap;
};
Because TypeScript compiler cannot safely match types compatibility.
Even if you explicitly tell the compiler it is completely the same whole function type or the same arguments type:
// β still cannot match proper type
const internalWrap: F = (...args) => f(...args);
const internalWrap2 = (...args: Parameters<F>) => f(...args);It is really a pity that such a simple idea like monkey patching cannot work safely in TypeScript and requires lies to compiler like ignoring errors or loosing type safety via any type.
