When working with TypeScript, there may be a challenge in understanding the correlation between the type of callbacks[key]
and the type of args
within the code line callbacks[key](...args)
. This difficulty arises when using the type Parameters<CallbackMap[K]>
, particularly when K
is a generic type parameter linked to a conditional type like the Parameters
utility type.
The issue at hand mirrors discussions seen in microsoft/TypeScript#30581; although that example focused on non-generic unions, here we deal with a generic conditional type restricted to a union.
A quick fix involves resorting to type assertions to silence compiler warnings:
const getWrappedCallback = <K extends keyof CallbackMap>(key: K) => {
return (...args: Parameters<CallbackMap[K]>) => {
console.log("Performing an additional preparation step");
(callbacks as any)[key](...args);
};
};
However, this method sacrifices some level of type safety since mistakes wouldn't be detected by the compiler. For greater assurance, a recommended approach similar to handling microsoft/TypeScript#30581 is advised, outlined in microsoft/TypeScript#47109. By explicitly structuring operations via indexed accesses into mapped types, TypeScript can more accurately comprehend the interaction between various types.
To implement this strategy, start by renaming callbacks
for clarity before constructing mapped types:
const _callbacks = {
alpha: (a: number, b: string) => { },
bravo: (c: boolean, d: object) => { },
charlie: (e: symbol, f: Date) => { },
} as const;
type _CallbackMap = typeof _callbacks;
Create two key types from these actions to differentiate parameter lists and return types from callbacks
:
type CallParams = { [K in keyof _CallbackMap]: Parameters<_CallbackMap[K]> }
type CallRets = { [K in keyof _CallbackMap]: ReturnType<_CallbackMap[K]> }
Rebuild callbacks
based on the mapped types introduced:
type CallbackMap = { [K in keyof _CallbackMap]: (...args: CallParams[K]) => CallRets[K] };
const callbacks: CallbackMap = _callbacks;
The new CallbackMap
type aligns closely with _CallbackMap
but is written explicitly via a mapped type structure, enhancing comprehension for the compiler:
const getWrappedCallback = <K extends keyof CallbackMap>(key: K) => {
return (...args: CallParams[K]) => {
const r = callbacks[key](...args); // no issues
// const r: CallRets[K]
};
};
Following these adjustments, compilation occurs without errors as the relationship between args
and callbacks[key]
is clarified through indexed access types rather than generic conditional ones.
Try out the code in the TypeScript Playground