A groupBy function takes an array of objects and a key, then returns a dictionary mapping each unique key value to an array of items. Writing one in TypeScript is where it gets awkward: the plain version has no type safety, and it breaks as soon as you group by an object.
Example input:
const items = [
{
id: 1,
name: "John",
birthday: {
year: 1990,
month: 1,
day: 1,
},
},
];A basic version looks like this:
const groupBy = (input, key) =>
input.reduce((acc, item) => {
const groupKey = item[key];
if (!(groupKey in acc)) {
acc[groupKey] = [];
}
acc[groupKey].push(item);
return acc;
}, {});But there are issues:
Since JavaScript object keys must be string | number | symbol, I updated the function:
const groupBy = (input, key) =>
input.reduce((acc, item) => {
const isTypeSupported = ["string", "number", "symbol"].includes(
typeof item[key],
);
const groupKey = isTypeSupported ? item[key] : JSON.stringify(item[key]);
return {
...acc,
[groupKey]: [...(acc[groupKey] || []), item],
};
}, {});Also, I chose not to mutate the accumulator and instead return a new object each time.
Now let’s add types:
const groupBy = <T extends Record<string, unknown>>(input: T[], key: keyof T) =>This is my starting point but it’s not enough and we’ll get Type 'string | T[keyof T]' cannot be used to index type '{}'. because type of groupKey is string | T[keyof T] and the second part is where the problem is.
To make it type-safe:
const groupBy = <
T extends Record<string, unknown>,
K extends keyof T,
V extends T[K] extends string | number | symbol ? T[K] : string,
>(
items: T[],
key: K,
) =>
items.reduce(
(acc, item) => {
const keyValue = ["string", "number", "symbol"].includes(typeof item[key])
? (item[key] as V)
: (String(item[key]) as V);
return {
...acc,
[keyValue]: [...(acc[keyValue] || []), item],
};
},
{} as Record<V, T[]>,
);Caveats:
This works for now, but there’s room for improvement.
Newer runtimes ship Object.groupBy and Map.groupBy for the same job. Their TypeScript types return a partial record, so every group can still be undefined at the call site.
For production code, lodash’s groupBy is the safer option. Its DefinitelyTyped signature returns Dictionary<T[]>, which is Record<string, T[]>, so the key type is wider than the Record<V, T[]> above.
tl;dr
A basic groupBy in JS isn’t enough if you care about types and edge cases. We explored how to build a version that’s type-safe, avoids mutating state, and handles object keys with JSON.stringify. Still, lodash’s groupBy is more reliable for production.
Occasional notes on software, tools, and things I learn. No spam.
Unsubscribe anytime.