I want to map an object and preserve types. I know how to do this with a single object, as such:
type MapTo<T, U> = {
[P in keyof T]: U
}
type ValueOf<T> = T[keyof T];
export const mapObject = <ObjectType extends object, MapType>(
value: ObjectType,
map: (v: ValueOf<ObjectType>) => MapType,
): MapTo<ObjectType, MapType> => {
const mappedValue = {} as MapTo<ObjectType, MapType>;
// Can't use `Object.keys` because it returns string[]
for (const i in value) {
if (value.hasOwnProperty(i)) {
mappedValue[i] = map(value[i]);
}
}
return mappedValue;
};
a
can have a varying number of keys, but the values are the same type. Then, if I do the following:
const a = { x: 1, y: 2, z: 3 };
const b = mapObject(a, (i) => i * 2);
b
still has the key completion (i.e., typing b.
will autosuggest the keys of the object, x
and y
).
Now, say I have the following code (apologies for the length!):
type MapTo<T, U> = {
[P in keyof T]: U;
}
type Map<T> = {
[P in keyof T]: T[P];
}
type ValueOf<T> = T[keyof T];
export const mapObjectTo = <ObjectType extends object, MapType>(
value: ObjectType,
map: (v: ValueOf<ObjectType>) => MapType,
): MapTo<ObjectType, MapType> => {
const mappedValue = {} as MapTo<ObjectType, MapType>;
// Can't use `Object.keys` because it returns string[]
for (const i in value) {
if (value.hasOwnProperty(i)) {
mappedValue[i] = map(value[i]);
}
}
return mappedValue;
};
export const mapObject = <ObjectType extends object>(
value: ObjectType,
map: (v: ValueOf<ObjectType>) => ObjectType[Extract<keyof ObjectType, string>],
): Map<ObjectType> => {
const mappedValue = {} as Map<ObjectType>;
// Can't use `Object.keys` because it returns string[]
for (const i in value) {
if (value.hasOwnProperty(i)) {
mappedValue[i] = map(value[i]);
}
}
return mappedValue;
};
const groups = {
a: {
x: 1,
},
b: {
y: 2
},
};
const mapGroupedObject = <ObjectType extends object, ReturnType>(
groupedObject: ObjectType,
map: (i: ValueOf<ObjectType[Extract<keyof ObjectType, string>]>) => ReturnType
) => {
const mapGroups = <T extends object>(item: T) => {
return mapObjectTo(item, map);
};
const mappedGroups = mapObject(groups, mapGroups);
return mappedGroups;
};
const mappedGroups = mapGroupedObject(groups, () => 2);
mappedGroups.a.x;
Essentially, mapGroupedObject
should be made up of n keys, which are all objects. For each of those objects, I want to map the values with a callback, and preserve the autocompletion
Again, mappedGroups
has autocompletion, so types seem to be resolving the structure, but the types don't seem to work here for mapGroups
argument in const mappedGroups = mapObject(groups, mapGroups);
in mapGroupedItems
. Also, I can't use a callback with a parameter for mapping because the types are unhappy when I do so
In fact, most of the types I've inferred from the previous functions mapObject
and mapObjectTo
. I'm really not sure what the hell ObjectType[Extract<keyof ObjectType, string>]
really means, it was presented by static code analysis in VSCode.
What do I need to do to satisfy the type compiler? Also, is there a way to generalise mapObject
and mapObjectTo
into a single function, if there's only one type in the generic?
As per request to the comment, for the groups
object in the second code block:
const groups = {
a: {
x: 1,
},
b: {
y: 2
},
};;
const mappedGroups = mapGroupedObject(groups, (item) => item * 2);
/* Outputs:
{
a: {
x: 2,
},
b: {
y: 4
},
}
And very importantly, preserves the structure in the type -
that is:
- typing `mappedGroups.` should suggest `mappedGroups.a` and `mappedGroups.b`,
- typing `mappedGroups.a.` should suggest `mappedGroups.a.x`
*/
CodePudding user response:
If we define a nested record type:
type GroupType = Record<string, Record<string, unknown>>;
a type to infer
the unknown types of the "leaf" values from a GroupType
:
type ValOf<Groups extends GroupType> =
Groups extends Record<string, Record<string, infer V>> ? V : never;
and a type to map these "leaf" values to a different type:
type MapRet<Groups extends GroupType, Ret> =
{ [KG in keyof Groups]: { [KGV in keyof Groups[KG]]: Ret } } & {}
(here we use & {}
to expand the type in intellisense... it can be omitted if that doesn't bother you)
Now we can define a function to implement your needs:
function map<G extends GroupType, Ret>(g: G, m: (v: ValOf<G>) => Ret):
MapRet<G, Ret> {
return Object.fromEntries(
Object.entries(g).map(
([k, v]) => [
k,
Object.fromEntries(
Object.entries(v).map(([ki, vi]) => [ki, m(vi as ValOf<G>)])
)
])
) as MapRet<G, Ret>
}
and call it:
const groups = {
a: {
x: 1,
},
b: {
y: 2
},
};
const test = map(groups, v => `hello${v}`);
/*
typeof test:
{
a: {
x: string;
};
b: {
y: string;
};
}
*/
console.log(test);
for output of
{
"a": {
"x": "hello1"
},
"b": {
"y": "hello2"
}
}