Consider a list of simple functions with different arguments:
const fns = {
isValidDate: (input: string, min?: Date, max?: Date): boolean => {
// ...
return true;
},
isValidOption: (input: string, options: string[]): boolean => {
// ...
return true;
},
};
They all return the same type (bool);
Then another function that is supposed to call any of the functions above:
function validateField(where: string, fn: keyof typeof fns, ...args: any[]){
// ...
return fns[fn](...args);
}
How can I make args
reflect the parameters of the chose fn
?
For example:
validateField("test", "isValidDate", new Date()); // should be ok
validateField("test", "isValidDate", 123); // should fail
and have the arguments show in vscode hints, like on normal functions.
I know I need to create overloads for validateField
for each fn
, but how to do that with a type definitions or something... without having to manually define each overload and write duplicate code with those arguments
CodePudding user response:
You probably want validateField()
to be generic in the type of the fn
parameter so you can choose the appropriate type for args
. We can write some helper utility types to compute this:
type Fns = typeof fns;
type FnArgs = { [K in keyof Fns]:
Fns[K] extends (input: string, ...args: infer A) => boolean ? A : never
};
/* type FnArgs = {
isValidDate: [min?: Date | undefined, max?: Date | undefined];
isValidOption: [options: string[]];
} */
The FnArgs
type is a mapped type where each key comes from the type of fns
, and each value is the tuple of parameters after the initial string
(using conditional type inference to get that list).
Now you can give validateField()
this call signature:
declare function validateField<K extends keyof Fns>(
where: string, fn: K, ...args: FnArgs[K]
): boolean;
and it will work when you call it:
validateField("test", "isValidDate", new Date()); // okay
validateField("test", "isValidDate", 123); // error! number is not assignable to date
validateField("test", "isValidOption", ["a"]) // okay
Unfortunately the implementation of validateField()
does not type check:
function validateField<K extends keyof Fns>(where: string, fn: K, ...args: FnArgs[K]) {
return fns[fn](where, ...args); // error!
// -----------------> ~~~~~~~
// A spread argument must either have a tuple type or
// be passed to a rest parameter.
}
The underlying issue is lack of direct support for correlated unions as requested in microsoft/TypeScript#30581. The compiler is not able to understand that the type of fns[fn]
is of a function type that is correlated with the type of args
. The error message is a bit cryptic, but it comes from the fact that it sees args
as a union of tuple types inappropriate for the arguments of fns[fn]
, which it sees as a union of function types without a common rest parameter type.
Luckily there's a recommended solution for this described in microsoft/TypeScript#47109. We need to give fns
a new type that the compiler can see at a glance is an object with methods whose parameters are directly related to FnArgs
. Here's how it looks:
function validateField<K extends keyof Fns>(where: string, fn: K, ...args: FnArgs[K]) {
const _fns: { [K in keyof Fns]: (str: string, ...args: FnArgs[K]) => boolean } = fns;
return _fns[fn](where, ...args); // okay
}
The _fns
variable is annotated as being of a mapped type of methods explicitly with a rest parameter of tpe FnArgs[K]
for every K
in the keys of Fns
. The assignment of fns
to that variable succeeds, because it's the same type.
But the crucial difference is that _fns[fn](where, ...args)
succeeds where fns[fn](where, ...args)
fails. And that's because the compiler has kept track of the correlation across the generic K
between the type of _fns[fn]
and the type of args
.
And now you have something that works as desired for both the callers and the implementation of the function!
CodePudding user response:
To solve your question, you can use the Generic type to get the function type and then use this to get the type of the parameter
function validateField<key extends keyof typeof fns>(where: string, fn: key, ...options: Parameters<typeof fns[key]>): boolean {
const fnToCall = fns[fn];
return fnToCall(...options);
}