In the following discussion, I will refer to the object { a: libraryA, b: libraryB }
as libraryMap
for clarity:
const libraryMap = {
a: libraryA,
b: libraryB
};
If you want a single code block like libraryMap[lib][key]()
to have strong typing with a generic type L
for lib
, then the type of libraryMap
needs to be defined in a way that allows the compiler to establish the correlation. This approach is explained in detail in microsoft/TypeScript#47109. It deals with correlated union types and shifting from unions to generics, as discussed in microsoft/TypeScript#30581.
The concept here is that the type of libraryMap
should be viewed as a mapped type over a basic key-value structure so that when you use libraryMap[lib]
, it's seen as an indexed access into that type, establishing the relationship between the type L
.
To implement this, we first rename libraryMap
temporarily to _libraryMap
and infer its type using TypeScript's typeof
operator:
const _libraryMap = {
a: libraryA,
b: libraryB
}
type _LibraryMap = typeof _libraryMap;
Next, we define LibKey
as the mapped type between the keys of libraryMap
and the keys of the objects they point to:
type LibKey = { [L in keyof _LibraryMap]: keyof _LibraryMap[L] };
We also create LibRet
as the mapped type connecting the keys of libraryMap
with the return type of methods within those objects:
type LibRet = { [L in keyof _LibraryMap]:
ReturnType<_LibraryMap[L][keyof _LibraryMap[L]]>
};
If there are issues with the ReturnType
line, consider using the alternative version provided.
Finally, we reconstruct the type of libraryMap
as a mapped type over LibKey
using both LibMap
and LibRet
, leveraging _libraryMap
and annotating it as LibMap
:
type LibMap = { [L in keyof LibKey]: Record<LibKey[L], () => LibRet[L]> };
const libraryMap: LibMap = _libraryMap;
The successful compilation of this last line indicates that our LibMap
type aligns with that of _LibraryMap
. While it may seem like a lot of type manipulation without much impact, the compiler recognizes the difference - only LibMap
maintains the required form for correlation.
Now let's demonstrate this:
function createFactory<L extends keyof LibMap>(lib: L, key: LibKey[L]) {
const selectedLib = libraryMap[lib][key];
return function () {
return selectedLib();
};
}
This snippet compiles without errors, indicating that the return type of createFactory()
is () => LibRet[L]
. Therefore, within the function, libraryMap[lib]
is interpreted as type LibMap[L]
, equivalent to
Record<LibKey[L], () => LibRet[L]>
. Since
key
is of type
LibKey[L]
,
libraryMap[lib][key]</code (or <code>selectedLib
) is understood as
() => LibRet[L]
. Consequently,
selectedLib()
returns
LibRet[L]
.
Therefore, calling createFactory()
ensures strongly-typed results:
const myFunction = createFactory('a', 'func');
console.log(myFunction().toFixed(1)); // 0.0
// Successful execution, myFunction() treated as number
Access the Playground link to code