Had a chat with a colleague and thought it’d be helpful to write down my thought process around creating a groupBy function in TypeScript.
You provide an array of items and a key, and it returns a dictionary mapping each unique key value to an array of items.
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.
Until then, lodash’s groupBy is your friend—type-safe and efficient.
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.