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: never
is a trick here to allow path functions of different signatures with and without argumentsunknown
as 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.