I already had a groupBy function which receives an array of objects and a key. It is capable to group by a single property.
const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
key: U,
): { [key: string]: T[] }
Since I get a depth=1 object as a result, I was pretty satisfied with the return type being { [key: string]: T[] }
But now I need to extend this function to be able to group by multiple properties and create a nested object with depth=keys.length.
the groupBy function:
const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
key: U,
): { [key: string]: T[] } => objArr
.reduce((memo, x) => {
if (x[key]) {
const value = (x[key] as any).toString();
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
}
return memo;
}, {} as { [key: string]: Array<T> });
The groupByMulti function, that just calls the groupBy function recursively until it reaches the last grouping key.
const groupByMulti = <T extends Record<string, unknown>, U extends keyof T>(
arr: T[],
keys: U[],
propIndex = 0,
) => {
const grouppedObj = groupBy(arr, keys[propIndex]);
Object.keys(grouppedObj).forEach((key) => {
if (propIndex < keys.length - 1) {
grouppedObj[key] = groupByMulti(grouppedObj[key], keys, propIndex + 1);
}
});
return grouppedObj;
}
I was wondering if it is possible to build an object type with a fixed depth like:
groupByMulti(someArray, ['key1', 'key2']): {
[key: string]: {
[key: string]: T[]
}
}
// Depending on the keys length, for example
groupByMulti(someArray, ['key1', 'key2', 'key3']): {
[key: string]: {
[key: string]: {
[key: string]: T[]
}
}
}
I know that this might be possible only if the keys are a ReadonlyArray, but I could live with that. If not possible, How can I achieve any amount of type-safety here?
Example converted to JS, to exemplify:
const cars = [{
car: 'Audi',
model: 'A6',
style: 'Sedan',
year: '2005',
},
{
car: 'Audi',
model: 'A4',
style: 'Sedan',
year: '2018',
},
{
car: 'Toyota',
model: 'Corola',
style: 'Sedan',
year: '2006',
},
{
car: 'Toyota',
model: 'Camry',
style: 'Sedan',
year: '2006',
},
]
const groupBy = (
objArr,
property,
) => objArr
.reduce((memo, x) => {
if (x[property]) {
const value = (x[property]).toString();
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
}
return memo;
}, {});
const groupByMulti = (arr, keys, propIndex = 0) => {
var grouppedObj = groupBy(arr, keys[propIndex]);
Object.keys(grouppedObj).forEach((key) => {
if (propIndex < keys.length - 1) {
grouppedObj[key] = groupByMulti(grouppedObj[key], keys, propIndex + 1);
}
});
return grouppedObj;
}
console.log(JSON.stringify(groupByMulti(cars, ['car', 'year', 'model']), null, 2));