Home > OS >  Typescript Inferring parameter types from function records
Typescript Inferring parameter types from function records

Time:02-08

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!

  •  Tags:  
  • Related