I'm tyring to create a memoize function in typescript and the function that should be able to be memoized must only accept simple types.
type ArgTypes<T> = T extends (...a: infer A) => unknown ? A : [];
type FuncWithSimpleParams = (...args: (number | string | boolean | null | undefined)[])
=> any;
export function memoize<T extends FuncWithSimpleParams>(func: T) {
const cache = {}
return function wrapper(...args: ArgTypes<T>) {
const cacheKey = args.join('.');
const cacheHit = cache.get(cacheKey);
if (cacheHit) {
return cacheHit;
}
const result = func(...args);
cache[cacheKey] = result;
return result;
};
}
But when I use it like this
memoize(function (first: string, second: string) {
return `${first}${second}`
})
I get typescript error saying
Types of parameters 'first' and 'args' are incompatible. Type 'string | number | boolean | null | undefined' is not assignable to type 'string'.
CodePudding user response:
This function type:
type FuncWithSimpleParams = (...args: (number | string | boolean | null | undefined)[])
=> any;
Declares a function that you can call with any of those arguments. But you pass it a function that can only take string
arguments.
"But, Alex", you're saying "T extends FuncWithSimpleParams
should allow for that, right?"
Well, no.
type Test = // This is `false`
((a: 1) => void) extends ((a: 1 | 2) => void)
? true
: false
So function arguments can't be extended this way.
You really have the wrong generic. Instead you want the arguments to your function as the generic type so you don't have to worry about the complex semantics of extending function types.
For example:
type SimpleType = number | string | boolean | null | undefined
type FuncWithSimpleParams<T extends SimpleType> = (...args: T[]) => any;
Now FuncWithSimpleParams
has a generic. That means the result will only allow a subset of SimpleType
as an argument, whatever subset that is the generic parameter.
Now memoize
becomes:
export function memoize<T extends SimpleType>(func: FuncWithSimpleParams<T>) {
And everything works as expected on this playground
Furthermore, now you get rid of ArgTypes
entirely (which was just Parameters<T>
anyway), and just use T[]
instead, since you know the argument type without having to interrogate the function explicitly.
export function memoize<T extends SimpleType>(func: FuncWithSimpleParams<T>) {
const cache = {}
return function wrapper(...args: T[]) { // just T[] now
const cacheKey = args.join('.');
const cacheHit = cache.get(cacheKey);
if (cacheHit) {
return cacheHit;
}
const result = func(...args);
cache[cacheKey] = result;
return result;
};
}
When you get to the right answer, it's amazing how everything seems to simplify itself.
This almost works. My example should have used different types for arguments. If you change the "second" param to number. You get the error I was referring to
If you want to allow any number of arguments of different types, then your generic needs to be an array type, which allows it to infer the arguments as a tuple type of known length where each index has a specific type.
type SimpleType = number | string | boolean | null | undefined
type FuncWithSimpleParams<T extends SimpleType[]> = (...args: T)
=> any;
export function memoize<T extends SimpleType[]>(func: FuncWithSimpleParams<T>) {
const cache = {}
return function wrapper(...args: T) {
const cacheKey = args.join('.');
const cacheHit = cache.get(cacheKey);
if (cacheHit) {
return cacheHit;
}
const result = func(...args);
cache[cacheKey] = result;
return result;
};
}
memoize(function (first: string, second: number) {
return `${first}${second}`
})