A try at type-safe groupBy function in TypeScript

April 10, 2025 · Updated on April 11, 2025

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:

  1. No type safety.
  2. If you group by a nested object (like birthday), the key becomes [object Object].

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:

  • Assumes items have string keys
  • Casts key values, which might be unsafe
  • No support for nested grouping—JSON.stringify() is the workaround

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.