I am currently attempting to create a higher-order function that wraps the input function and caches the result of the most recent call as a side effect. The basic function (withCache
) is structured as follows:
function cache(key: string, value: any) {
//Caching logic implemented here
}
function withCache<R>(key: string, fn: (...args: any[]) => R): (...args: any[]) => R {
return (...args) => {
const res = fn(...args);
cache(key, res);
return res;
}
}
const foo = (x: number, y: number) => x + y;
const fooWithCache = withCache("foo", foo);
let fooResult1 = fooWithCache(1, 2); // allowed :)
let fooResult2 = fooWithCache(1, 2, 3, 4, 5, 6) // also allowed :(
To ensure type safety, I can use function overloads like this:
function withCache<R>(key: string, fn: () => R): () => R
function withCache<R, T1>(key: string, fn: (a: T1) => R): (a: T1) => R
function withCache<R, T1, T2>(key: string, fn: (a: T1, b: T2) => R): (a: T1, b: T2) => R
function withCache<R>(key: string, fn: (...args: any[]) => R): (...args: any[]) => R {
// implementation ...
}
const foo = (x: number, y: number) => x + y;
const fooWithCache = withCache("foo", foo);
let fooResult1 = fooWithCache(1, 2); // allowed :)
let fooResult2 = fooWithCache(1, 2, 3, 4, 5, 6) // not allowed :)
The challenge arises when trying to allow functions with optional arguments where Typescript isn't selecting the correct overload for withCache
, resulting in an unexpected signature for fooWithCache
. Is there a way to resolve this issue?
(As a side note, is there any way to declare the overloads so I don't have to repeat each overload's function type (...) => R
?)
Edit:
Resolved my secondary question about repetitive function type declarations by defining it separately:
type Function1<T1, R> = (a: T1) => R;
// ...
function withCache<T1, R>(fn: Function1<T1, R>): Function1<T1, R>;
Edit:
How would this work for an asynchronous function (assuming you wanted to cache the result and not the Promise itself)? You could certainly do this:
function withCache<F extends Function>(fn: F) {
return (key: string) =>
((...args) =>
//Wrap in a Promise so we can handle sync or async
Promise.resolve(fn(...args)).then(res => { cache(key, res); return res; })
) as any as F; //Really want F or (...args) => Promise<returntypeof F>
}
However, using this approach would be unsafe with synchronous functions:
//Async function
const bar = (x: number) => Promise.resolve({ x });
let barRes = withCache(bar)("bar")(1).x; //Not allowed :)
//Sync function
const foo = (x: number) => ({ x });
let fooRes = withCache(foo)("bar")(1).x; //Allowed, but TS assumes fooRes is an object :(
Is there a safeguard against this? Or a way to create a function that works safely for both scenarios?
Summary: @jcalz's answer is correct. In cases where synchronous functions can be assumed, or where working directly with Promises instead of their resolved values is acceptable, asserting the function type might be safe. However, handling the sync-or-async situation described above necessitates language improvements that are still pending development.