The issue arises when Parameters<FN>
is not recognized as spreadable in TypeScript, which has been highlighted as a bug and reported on microsoft/TypeScript#36874. To address this, the workaround involves altering the constraint from (...args: any) => ⋯
to (...args: any[]) => ⋯
:
function memoize<FN extends (...args: any[]) => Promise<any>>(fn: FN) {
const cache: Record<string, Promise<Awaited<ReturnType<FN>>>> = {};
return (...args: Parameters<FN>) => {
const cacheKey = JSON.stringify(args);
if (!(cacheKey in cache)) {
const promise = fn(...args); // okay
cache[cacheKey] = promise;
}
return cache[cacheKey];
};
}
However, it's important to note that this approach is not recommended.
The utility types Awaited
, Parameters
, and ReturnType
are implemented using conditional types. Within the scope of memoize()
, the type FN
serves as a generic parameter, making Parameters<FN>
and
Awaited<ReturnType<FN>>
generic conditional types. TypeScript struggles with analyzing such types effectively, often deferring their evaluation. Consequently,
Parameters<FN>
remains opaque within
memoize()
, leading to ambiguity in checking arguments passed to
fn(...args)
.
By restricting FN
to (...args: any[]) => ⋯
, invoking f(...args)
widens FN
to its constraint, allowing acceptance of any argument despite potential errors, like fn(123)
:
const promise = fn(123); // okay?!!!
Cautious consideration must be taken if opting for this method due to the pitfalls associated with generic conditional types.
The preferred alternative to generic conditional types involves making the function generic based on its argument list A
and return type R
, rather than the complete function type FN
:
function memoize<A extends any[], R>(fn: (...args: A) => Promise<R>) {
const cache: Record<string, Promise<R>> = {};
return (...args: A) => {
const cacheKey = JSON.stringify(args);
if (!(cacheKey in cache)) {
const promise = fn(...args); // okay
cache[cacheKey] = promise;
}
return cache[cacheKey];
};
}
This methodology results in clean compilation, enabling the compiler to accurately interpret situations. For instance, attempting to execute fn(123)
would yield an expected error:
const promise = fn(123); // error!
// ~~~
// Argument of type '[number]' is not assignable to parameter of type 'A'.
Explore the Playground link to code