The compiler struggles with understanding type safety in situations where generic type parameters are not yet specified. This issue is detailed in a GitHub problem report at microsoft/TypeScript#24085.
There is a rare chance that the function might infer K
as Keys
itself instead of specific values like "sum"
or "concat"
. For example:
const oops = apply(Math.random() < 0.5 ? "sum" : "concat", "a", "b", "c");
console.log(oops); // Result can be "abc" or "ab"
In this scenario, the compiler technically points out that the operation is not entirely type safe. To resolve this limitation, there is a feature proposal in microsoft/TypeScript#27808.
Furthermore, the compiler does not view the funKey
parameter and the args
rest parameter as having interconnected types. Even if it could, maintaining that correlation proves to be challenging.
Since the compiler may struggle to compute the return type on its own, manual annotations become necessary. The ReturnType<F>
utility type comes in handy for this purpose. Additionally, you can utilize the Parameters<F>
utility type instead of creating Args<F>
from scratch.
To tackle these limitations, users often need to assure the compiler about the type safety of their operations due to the compiler's inability to verify it. A potential solution involves using a type assertion, such as any
:
type Funs = typeof funs;
function apply<K extends Keys>(funKey: K, ...args: Parameters<Funs[K]>): ReturnType<Funs[K]> {
return (funs[funKey] as any)(...args);
}
This approach enables unconventional operations like
return (funs[funKey] as any)(true)
, highlighting the importance of cautious usage. Another, slightly more complex method involves representing
funs[funKey]
as a function that can handle various argument types and return multiple possible result types:
type WidenFunc<T> = ((x: T) => void) extends ((x: (...args: infer A) => infer R) => any) ?
(...args: A) => R : never;
function apply<K extends Keys>(funKey: K, ...args: Parameters<Funs[K]>): ReturnType<Funs[K]> {
return (funs[funKey] as WidenFunc<Funs[Keys]>)(...args);
}
The resultant function type WidenFunc<Funs[Keys]>
becomes
(...args: [number, number] | [string, string, string]) => number & string
. While seemingly nonsensical, this type prevents incorrect arguments like
(true)
from being passed.
Either of these methods should suffice:
const test1 = apply('sum', 1, 2) // number
const test2 = apply('concat', 'str1', 'str2', 'str3') // string
Hopefully, this information proves helpful. Good luck!
Playground link to code