I'm trying to write a higher-order function wraps the input function and caches the result of the most recent call as a side effect. The basic function (withCache) looks something like this:
function cache(key: string, value: any) {
//Some caching logic goes 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 :(
Now I know I can make this type safe - up to a point - using function overloads, like so:
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 trouble comes when I try to allow functions with optional arguments (the last overload is new):
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, 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 || 0);
const fooWithCache = withCache("foo", foo);
let fooResult1 = fooWithCache(1); // allowed :)
let fooResult2 = fooWithCache(1, 2) // not allowed, but should be :(
The problem seems to be that Typescript is picking the wrong overload for withCache, and the result is that the signature for fooWithCache is (a: number) => number. I would expect fooWithCache's signature to be (a: number, b?: number) => number, just like foo.
Is there any way to fix this?
(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:
Figured out my secondary question about not repeating the function type: just define it!
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>
}
But then it would be unsafe to use with a synchronous function:
//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, because TS thinks fooRes is an object :(
Is there a way to guard against this? Or to write a function that safely works for both?
Summary: @jcalz's answer is correct. In cases where synchronous functions can be assumed, or where it's okay to work with Promises directly and not the values they resolve to, asserting the function type is probably safe. However, the sync-or-async scenario described above is not possible without some as-yet unimplemented language improvements.
let fooResult2 = fooWithCache(1, 2)as per your code?fooWithCacheto be(a: number, b?: number) => number, just likefoo.console.log(fooWithCache)when you useconst foo = (x: number, y?: number) => x + (y || 0);have you tried usingconst foo = (x: number, y: number = 0) => x + y ;setting a default value for 2nd parameter.