I have a object fn
with name:function pairs that can take arbitrary parameters. I want to be able to map these functions in order to wrap them with another default function to handle errors in a single location. I have the following code which doesn't even compile giving A spread argument must either have a tuple type or be passed to a rest parameter.
on fn[f](...args)
const fn = {
...userActions,
...companyActions,
...dataActions
};
type A<T extends Array<any>, U> = Record<keyof typeof fn, (...args: T) => Promise<U>>;
function map<T extends Array<any>, U>(f: keyof A<T, U>, ...args: T) {
fn[f](...args);
}
I can wrap functions using the code below but it requires importing the relevant function into the caller file and passing it into fn
parameter
async function run<T extends Array<any>, U>(fn: (...args: T) => Promise<U>, ...temp: Parameters<typeof fn>) {
await fn(...temp); // works but needs to pass the function itself.
}
Is there a way to get around the issue on first piece of code? Basically I want to infer the parameter type by using just the key from a record.
CodePudding user response:
The problem here is that, in your map
function, Typescript does not infer that keyof typeof fn
is one particular key of fn
, but rather that it is the union of all possible keys of fn. It won't resolve the particular key until you actually pass it a concrete type when you call map. This in turn means that args
is being resolve as the intersection of all possible args of all functions in fn
--not a spread of one particular args
.
This is a typescript limitation as TS won't resolve the generic inside a function until it is actually passed a concrete type.
So, unfortunately, I do think some assertion is necessary inside the map
function, but that doesn't prevent you from having good type safety on the inputs and outputs of your map
function.
That said, I wasn't 100% clear on how you were hoping to use your map
function, but hopefully this helps understand a way to type this kind of problem:
const userActions = {
func1: (x:string)=>parseInt(x)
}
const companyActions = {
func2: (x:number)=>x.toString()
}
const fn = {
...userActions,
...companyActions,
};
function map<T extends keyof typeof fn>(f: T, ...args: Parameters<typeof fn[T]>):ReturnType<typeof fn[T]> {
return (fn[f] as any)(args) as ReturnType<typeof fn[T]>;
}
map("func1", "foo")
map("func2", 3)
//these will error
map("func1", 3)
map("func2", "string")
With a playground. This will ensure the inputs and output of map
are typed based on fn
.
I removed the Promise as I don't think that's fundamental to your question. It's trivial to add that back in. Please post more info if this isn't sufficient.
CodePudding user response:
I think your mistake is, you didn't specify the type of the fn
object, so the typescript couldn't understand whatever you want from him.
As a test I have specified the type the following way:
const fn: { [key: string]: (...args: any[]) => any } = {
...userActions,
...companyActions,
...dataActions
};
This way you letting the typescript to know that any of the fn
properties can take a certain amount of parameters.
And now it compiles and works when I call the map
function!