1. Omid Sayfun
  2. /
  3. Notebook
  • Home
  • About
  • Notebook
  • Token Usage
  • Whisper Usage
Tools
  • Agent Knowledge Base

A try at type-safe groupBy function in TypeScript

April 10, 2025 · Updated on August 09, 2026

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.

I write short, practical notes like this one. Get the next one by email:

Unsubscribe anytime.

A plain JavaScript groupBy breaks on object keys

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.

Making the TypeScript groupBy type-safe

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

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

Join My Newsletter

Occasional notes on software, tools, and things I learn. No spam.

Unsubscribe anytime.

Continue Reading

  • Embeddings rot too. Running pgvector in production.Aug 28, 2026
  • Stop shipping retrieval changes on vibesAug 25, 2026
  • Your RAG is confidently wrong without hybrid searchAug 21, 2026
  • Stop tuning everything. pgvector has three knobs that matter.Aug 18, 2026
  • Your agent's knowledge base is lying to you. Run these 14 checks.Aug 18, 2026