---
title: "A try at type-safe groupBy function in TypeScript"
description: "Build a type-safe TypeScript groupBy for an array of objects, handle object keys and nested values, and compare it with Object.groupBy and lodash's groupBy."
canonical: https://omiid.me/notebook/45/a-try-at-type-safe-groupby-function-in-typescript
author: Omid Sayfun
published: 2025-04-10
updated: 2026-08-09
---

# A try at type-safe groupBy function in TypeScript

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.

## A plain JavaScript groupBy breaks on object keys

Example input:

```javascript
const items = [
	{
		id: 1,
		name: "John",
		birthday: {
			year: 1990,
			month: 1,
			day: 1,
		},
	},
];
```

A basic version looks like this:

```javascript
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:

```javascript
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.

## Making the TypeScript groupBy type-safe

Now let’s add types:

```typescript
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:

```typescript
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[]>,
	);
```

## What this groupBy still gets wrong

Caveats:

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

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](https://lodash.com/docs/#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.
