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 type G
where any generic type parameters on F
get transferred to G
.
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 if f
is a function of type F
, and const g = wrapFn(f)
, it is possible to write wrapFn()
so that g
is of type G
, where any generic type parameters of F
have been transferred to G
.
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 like F extends (...args: any[])=>any
where you use conditional utility types like Parameters<F>
or ReturnType<F>
to extract argument types or return types from it.
So, if you change WrappedFn
and wrapFn
to the folllowing:
type WrappedFn<A extends any[], R> = (...parameters: A) => Promise<R>
function wrapFn<A extends any[], R>(fn: (...args: A) => R): WrappedFn<A, R> {
return async (...parameters: A) => {
console.log(`Parameters are ${parameters}`);
const result = await fn(...parameters);
console.log(`Result is ${result}`);
return result;
}
}
Then things behave as you expect, at least for the example code in your question:
const wrappedDelayValue = wrapFn(delay);
// const wrappedDelayValue: <V>(value: V, ms: number) => Promise<Promise<V>>
const delayedValue = await wrappedDelayValue("hello", 1000);
// const delayedValue: string
console.log(delayedValue.toUpperCase()) // no compiler error now