← run

ts-02-groupby

1.000
6/6 tests· typing
Challenge · difficulty 2/5
# Generic `groupBy`

Implement **`solution.ts`** exporting a generic function:

```ts
export function groupBy<T, K extends string | number>(
  items: T[],
  keyFn: (item: T) => K,
): Record<K, T[]>
```

Group the `items` into a record keyed by the result of `keyFn(item)`. Each value is
the array of items that produced that key, **in their original order**.

- `groupBy([1, 2, 3, 4], (n) => (n % 2 === 0 ? "even" : "odd"))`
  → `{ odd: [1, 3], even: [2, 4] }`
- `groupBy([], (n: number) => n)` → `{}`
- A numeric key works too:
  `groupBy(["a", "bb", "ccc", "dd"], (s) => s.length)`
  → `{ 1: ["a"], 2: ["bb", "dd"], 3: ["ccc"] }`

Keep it fully typed (must pass `tsc --noEmit` in strict mode). Do not use `any` in the
public API.
tests/solution.test.ts
import { test } from "node:test";
import { strict as assert } from "node:assert";
import { groupBy } from "./solution.ts";

test("groups by string key, preserving order", () => {
  const r = groupBy([1, 2, 3, 4, 5], (n) => (n % 2 === 0 ? "even" : "odd"));
  assert.deepEqual(r, { odd: [1, 3, 5], even: [2, 4] });
});

test("empty input yields empty record", () => {
  assert.deepEqual(groupBy<number, number>([], (n) => n), {});
});

test("groups by numeric key", () => {
  const r = groupBy(["a", "bb", "ccc", "dd"], (s) => s.length);
  assert.deepEqual(r, { 1: ["a"], 2: ["bb", "dd"], 3: ["ccc"] });
});

test("groups objects by a property", () => {
  const people = [
    { name: "Ana", city: "NYC" },
    { name: "Bo", city: "LA" },
    { name: "Cy", city: "NYC" },
  ];
  const r = groupBy(people, (p) => p.city);
  assert.deepEqual(r, {
    NYC: [
      { name: "Ana", city: "NYC" },
      { name: "Cy", city: "NYC" },
    ],
    LA: [{ name: "Bo", city: "LA" }],
  });
});

test("single group when keyFn is constant", () => {
  const r = groupBy([1, 2, 3], () => "all");
  assert.deepEqual(r, { all: [1, 2, 3] });
});

test("does not mutate the input array", () => {
  const input = [3, 1, 2];
  groupBy(input, (n) => n);
  assert.deepEqual(input, [3, 1, 2]);
});
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```ts
export function groupBy<T, K extends string | number>(
  items: T[],
  keyFn: (item: T) => K,
): Record<K, T[]> {
  const result = {} as Record<K, T[]>;
  for (const item of items) {
    const key = keyFn(item);
    if (!result[key]) {
      result[key] = [];
    }
    result[key].push(item);
  }
  return result;
}
```

Test output (stdout)

TAP version 13
# Subtest: groups by string key, preserving order
ok 1 - groups by string key, preserving order
  ---
  duration_ms: 0.609262
  type: 'test'
  ...
# Subtest: empty input yields empty record
ok 2 - empty input yields empty record
  ---
  duration_ms: 0.06393
  type: 'test'
  ...
# Subtest: groups by numeric key
ok 3 - groups by numeric key
  ---
  duration_ms: 0.06611
  type: 'test'
  ...
# Subtest: groups objects by a property
ok 4 - groups objects by a property
  ---
  duration_ms: 0.07135
  type: 'test'
  ...
# Subtest: single group when keyFn is constant
ok 5 - single group when keyFn is constant
  ---
  duration_ms: 0.05813
  type: 'test'
  ...
# Subtest: does not mutate the input array
ok 6 - does not mutate the input array
  ---
  duration_ms: 0.04389
  type: 'test'
  ...
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 90.377735
Reasoning (chain-of-thought)
The user wants a generic `groupBy` function in TypeScript that:
1. Takes an array of items of type T
2. Takes a key function that maps each item to a key of type K (where K extends string | number)
3. Returns a Record<K, T[]> where items are grouped by their key
4. Items should maintain their original order within each group
5. Must be fully typed, no `any` in public API
6. Must pass `tsc --noEmit` in strict mode

Let me implement this:

```ts
export function groupBy<T, K extends string | number>(
  items: T[],
  keyFn: (item: T) => K,
): Record<K, T[]> {
  const result = {} as Record<K, T[]>;
  for (const item of items) {
    const key = keyFn(item);
    if (!result[key]) {
      result[key] = [];
    }
    result[key].push(item);
  }
  return result;
}
```

Wait, there's a potential issue with `as Record<K, T[]>` - this might not be fully type-safe. Let me think about this more carefully.

Actually, the issue is that TypeScript's `Record<K, T[]>` type means every key in K must be present. But in our case, we only want the keys that actually appear. However, the function signature given by the user specifies `Record<K, T[]>` as the return type, so we need to match that.

The `as Record<K, T[]>` cast is necessary because TypeScript can't track that we're building up the record dynamically. This is a common pattern and should be acceptable.

Let me verify the examples:
1. `groupBy([1, 2, 3, 4], (n) => (n % 2 === 0 ? "even" : "odd"))` → `{ odd: [1, 3], even: [2, 4] }` ✓
2. `groupBy([], (n: number) => n)` → `{}` ✓
3. `groupBy(["a", "bb", "ccc", "dd"], (s) => s.length)` → `{ 1: ["a"], 2: ["bb", "dd"], 3: ["ccc"] }` ✓

The implementation looks correct. Let me write it out cleanly.