When dealing with TypeScript, there is no straightforward approach to achieve this task without resorting to methods like type assertions. The language lacks the necessary expressive power to abstract over mapped types in a broad sense, especially without higher kinded types as indicated in microsoft/TypeScript#1213.
A possible solution could be:
const WrapSet = (inner: FuncSet): FuncSet =>
Object.fromEntries(Object.entries(inner).map(([k, v]) => [k, Wrap<any>(v)])) as
unknown as FuncSet;
This approach involves using Object.entries()
to break down the object into its attributes, then map
ping them to form new attributes, and finally utilizing Object.fromEntries()
to reconstruct the object. Despite working effectively at runtime, this method employs type assertions and the any
type to avoid type-related issues.
Attempting to enhance type safety would encounter obstacles related to higher-order generics and their constraints. While you may desire to define WrapSet
as a specialized version of the more general mapper mapValues()
:
function mapValues<T extends object>(obj: T, map: <K extends keyof T>(v: T[K]) => T[K]) {
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, map(v)])) as T
}
Convincing the compiler that Wrap
possesses the correct type becomes exceedingly difficult:
const WS = (inner: FuncSet) => mapValues(inner, Wrap); // error!
// -------------------------------------------> ~~~~
The suitable type definition appears as follows:
const Wrap = <N extends keyof Properties>(inner: FuncSet[N]): FuncSet[N] => {
return (v: Properties[N]) => inner(v) + 2; // error!
//~~~~~~
};
However, the compiler struggles to grasp the intricate higher-order relationship in this scenario. While some manipulation of generics might bring you closer, the efforts may not be worthwhile in the end. For now, utilizing the type assertion version seems prudent, until TypeScript incorporates higher-order generics, which may not happen at all.
Playground link to code