I am attempting to wrap typescript functions, returning new functions that add extra behaviour. For example, in the minimal repro below, (see playground) the function after wrapping always returns a Promise and its arguments and return value are console-logged.
However, I have found that if the original function has any Generic typing, I don't know how to bind that Generic type so it is available on the wrapped function even if it is well-known in the calling scope.
The problem is demonstrated by const delayedValue
in the below script. Typescript seems to believe this can only be unknown.
Is there any way for e.g. wrappedDelay
to be defined such that the generic V
parameter can make it through and hence inform what return type we can expect from the wrappedDelay
function.
export type AnyFn = (...args: any[]) => any;
/** If sync function - ReturnType. If async function - unwrap Promise of ReturnType */
export type Result<Fn extends AnyFn> = Fn extends () => Promise<infer Promised>
? Promised
: ReturnType<Fn>;
/** Async function with same params and return as Fn, regardless if Fn is sync or async */
export type WrappedFn<Fn extends AnyFn> = (...parameters:Parameters<Fn>) => Promise<Result<Fn>>
/** Construct wrapped function from function reference */
function wrapFn<Fn extends AnyFn>(fn:Fn) : WrappedFn<Fn>{
return async (...parameters:Parameters<Fn>) => {
console.log(`Parameters are ${parameters}`);
const result = await fn(...parameters);
console.log(`Result is ${result}`);
return result;
}
}
function sum(a:number, b:number){
return a + b;
}
function delay<V>(value: V, ms:number){
return new Promise<V>((resolve, reject) => {
setTimeout(() => resolve(value), ms)
})
}
const wrappedSum = wrapFn(sum)
const wrappedDelay = wrapFn(delay)
async function example() {
const sum = await wrappedSum(3,4)
const delayedValue = await wrappedDelay("hello", 1000)
}
TypeScript does not have direct support for "higher kinded types" of the sort requested in microsoft/TypeScript#1213, so there's no general way to manipulate generic functions programmatically in such a way as express a purely type-level transformation of the function type
F
to function typeG
where any generic type parameters onF
get transferred toG
.Luckily, since TypeScript 3.4, there is support for higher order type inference from generic functions where you can get behavior like this for particular functions at the value-level, such as
wrapFn()
acting on input functions which are generic. So iff
is a function of typeF
, andconst g = wrapFn(f)
, it is possible to writewrapFn()
so thatg
is of typeG
, where any generic type parameters ofF
have been transferred toG
.You can read microsoft/TypeScript#30215 for how this higher order type inference works and the rules you need to follow to get this behavior. In particular, this feature expects that you will have separate type parameters for the function arguments (e.g.,
A extends any[]
) and for the return (e.g.,R
). It does not work with a generic function type likeF extends (...args: any[])=>any
where you use conditional utility types likeParameters<F>
orReturnType<F>
to extract argument types or return types from it.So, if you change
WrappedFn
andwrapFn
to the folllowing:Then things behave as you expect, at least for the example code in your question:
Playground link to code