I am trying to build a function that takes a definition of a object with functions, that are different (but in a predictable way) from the actual functions provided.
e.g.:
converter<{
foo: (n: number) => void,
bar: (s: string) => void
}>({
foo: fooFunction,
bar: barFunction
})
// ...
function fooFunction(n: number) {
return () => {
// ...
}
}
function barFunction(s: string) {
return () => {
// ...
}
}
I've tried the following the code:
function converter<
Functions extends Record<string, (...a: any) => any>,
Key extends keyof Functions = keyof Functions
>(functions: Record<
Key,
(...a: Parameters<Functions[Key]>) => () => ReturnType<Functions[Key]>
>): Functions {
// ...
}
But this won't work, since the signature of foo
and bar
are different. With this error: Type '[s: string] | [n: number]' is not assignable to type '[s: string]'.
The problem is (...a: Parameters<Functions[Key]>) => () => ReturnType<Functions[Key]>
, as it is expanded for every element, thus Parameters<Functions[Key]>
has to match every function provided.
How do I ensure that the parameters are only expanded for the Key
they match? Is what I am doing even possible?
The purpose of converter()
is to convert these function-returning functions into the functions defined in Functions
.
CodePudding user response:
Record
s are for when many keys share a common type. That's not the case here.
You need to use a mapped type. This is because you need the specific key being operated on for each value. Record
does not provide this to you, but a mapped type does.
function converter<
Functions extends Record<string, (...a: any) => any>
>(functions: {
[Key in keyof Functions]:
(...a: Parameters<Functions[Key]>) => () => ReturnType<Functions[Key]>
}): Functions {
//...
}
Note how this allows you use Key
in the value of each property, and in that spot it is one specific key at a time because the mapped type distributes over the keys.
I would flip this all around, however. Generic type parameters for function work best if an argument is the generic. This lets Typescript infer the types for those generics best.
So then Functions
becomes the type of the argument, and the return value is derived from that.
declare function converter<
Functions extends Record<string, (...a: any) => () => any>
>(functions: Functions): {
[Key in keyof Functions]:
(...a: Parameters<Functions[Key]>) => ReturnType<ReturnType<Functions[Key]>>
}
And then refactor that mouthful to something like:
type FlattenedFn<T extends (...args: any) => () => any> =
(...args: Parameters<T>) => ReturnType<ReturnType<T>>
declare function converter<
Functions extends Record<string, (...a: any) => () => any>
>(functions: Functions): {
[Key in keyof Functions]: FlattenedFn<Functions[Key]>
}
And now you can call this without specifying the generic type at all.
const test = converter({
foo: fooFunction,
bar: barFunction
})