Upon inspection, it is evident that the compiler faces challenges in identifying the relationship between the types of FUNCTIONS[type]
and arg
. This issue mirrors the one outlined in microsoft/TypeScript#30581, where it revolves around correlated union types. The problem arises when the compiler attempts to invoke FUNCTIONS[type](arg)
, causing it to widen the generic type of FUNCTIONS[type]
to the union
((arg: string) => void) | (arg: number) => void))
, and the type of
arg
to
string | number
. Unfortunately, calling a function with an argument of a conflicting type results in errors as inferred by the compiler, making it impossible to execute smoothly.
function callFunction<K extends keyof FnArg>(type: K, arg: FnArg[K]) {
const fn = FUNCTIONS[type];
// const fn: (arg: never) => void
// (parameter) arg: string | number
return fn(arg); // error! 😢
}
However, a solution exists for this dilemma, detailed in microsoft/TypeScript#47109. The key lies in refining your types to be explicitly defined as actions on generic indexed accesses within mapped types over a base object type. This strategy aims to establish fn
as having the generic type (arg: XXX) => void
while ensuring arg
corresponds to the same generic XXX
. Therefore, adapting the type of
FUNCTIONS</code to map over the base type is imperative.</p>
<p>To accomplish this, you can follow these steps. Start by renaming the <code>FUNCTIONS
variable:
const _FUNCTIONS = {
foo,
bar,
} as const;
Subsequently, construct the base object type using the renamed variable:
type FnArg = { [K in keyof typeof _FUNCTIONS]:
Parameters<typeof _FUNCTIONS[K]>[0] }
/* type FnArg = {
readonly foo: number;
readonly bar: string;
} */
Declare FUNCTIONS
as a mapped type over FnArg
:
const FUNCTIONS: {
[K in keyof FnArg]: (arg: FnArg[K]) => void
} = _FUNCTIONS;
With these adjustments, you can utilize callFunction()
to operate on generic indexed accesses efficiently:
function callFunction<K extends keyof FnArg>(type: K, arg: FnArg[K]) {
const fn = FUNCTIONS[type];
// const fn: (arg: FnArg[K]) => void
// (parameter) arg: FnArg[K]
return fn(arg); // okay
}
This method resolves the previous hurdles admirably!
Note that the equivalency between the types of FUNCTIONS
and
_FUNCTIONS</code becomes apparent if examined closely:</p>
<pre><code>/* const _FUNCTIONS: {
readonly foo: (arg: number) => void;
readonly bar: (arg: string) => void;
} */
/* const FUNCTIONS: {
readonly foo: (arg: number) => void;
readonly bar: (arg: string) => void;
} */
The only distinction lies in their internal representation, where the mapped type of
FUNCTIONS</code allows the compiler to establish the correlation between <code>FUNCTIONS[type]
and
arg
. Swapping
FUNCTIONS
back to
_FUNCTIONS</code within <code>callFuction()
's body would reintroduce the initial error.
Overall, this challenge showcases a nuanced obstacle that has been expertly navigated through the approach highlighted in microsoft/TypeScript#47109, elucidating its functionality and importance.
Playground link to code