Langkau ke kandungan
WANCODE
EN

Utiliti groupBy Bertaip (TypeScript)

Kumpulkan array ke dalam kumpulan dengan inferens jenis penuh — dan fahami kenapa jenis pulangan yang jujur ialah Partial<Record<K, T[]>>.

groupBy.ts
/**
 * Groups items into arrays by a key you compute.
 *
 * groupBy(users, (u) => u.state)
 *   -> { Selangor: [...], Sabah: [...] }
 */
export function groupBy<T, K extends PropertyKey>(
  items: readonly T[],
  getKey: (item: T, index: number) => K,
): Partial<Record<K, T[]>> {
  // No prototype, so keys like "constructor" or "__proto__" are safe
  const groups: Partial<Record<K, T[]>> = Object.create(null);

  items.forEach((item, index) => {
    const key = getKey(item, index);
    (groups[key] ??= []).push(item);
  });

  return groups;
}

/** Same idea, but keeps any kind of key (objects, dates) and insertion order. */
export function groupByToMap<T, K>(
  items: Iterable<T>,
  getKey: (item: T) => K,
): Map<K, T[]> {
  const groups = new Map<K, T[]>();
  for (const item of items) {
    const key = getKey(item);
    const group = groups.get(key);
    if (group) group.push(item);
    else groups.set(key, [item]);
  }
  return groups;
}

Mengumpulkan data ialah antara kerja paling biasa: pesanan mengikut status, pelajar mengikut kelas, pengguna mengikut negeri. Logiknya hanya tiga baris. Bahagian yang menarik ialah jenis (types): TypeScript patut tahu apa yang ada dalam setiap kumpulan, dan jujur bahawa sesetengah kumpulan mungkin tiada.

Membaca signature

  • <T, K extends PropertyKey> mengisytiharkan dua generic: T ialah jenis item (diinferens daripada array anda) dan K ialah jenis kunci. PropertyKey bermaksud string | number | symbol — perkara yang boleh menjadi kunci objek.
  • readonly T[] berjanji kita tidak akan mengubah array anda, dan membolehkan anda menghantar array dan tuple readonly juga.
  • Jika getKey memulangkan "paid" | "pending", maka K tepat union itu, jadi groups.paid boleh autocomplete dan groups.paidd menjadi ralat jenis.
  • Partial<Record<K, T[]>> mengakui bahawa sesuatu kumpulan mungkin tiada (tiada pesanan pending hari ini), jadi TypeScript memaksa anda uruskan undefined. Record<K, T[]> biasa akan berbohong.
  • (groups[key] ??= []).push(item) bermaksud: jika kumpulan ini belum wujud, cipta array kosong; kemudian push ke dalamnya. ??= hanya menetapkan nilai bila sebelah kiri null atau undefined.
  • Object.create(null) mencipta objek tanpa prototype, jadi kunci bernama "constructor" tidak bertembung dengan property terbina.

Kini ada versi terbina

Object.groupBy() dan Map.groupBy() (ES2024) buat kerja yang sama dalam Chrome 117+, Firefox 119+, Safari 17.4+ dan Node.js 21+. TypeScript menyediakan jenisnya bila lib anda termasuk ES2024. Menulis versi sendiri tetap latihan yang bagus — dan berfungsi di mana-mana.

Kunci objek sentiasa string semasa runtime. Jika getKey memulangkan nombor, groups[2024] berfungsi, tetapi Object.keys(groups) memberi "2024". Guna groupByToMap bila kunci bukan string.

Cara guna

TypeScript
import { groupBy, groupByToMap } from "./groupBy";

type Order = { id: number; status: "paid" | "pending" | "refunded"; total: number };

const orders: Order[] = [
  { id: 1, status: "paid", total: 45 },
  { id: 2, status: "pending", total: 12 },
  { id: 3, status: "paid", total: 80 },
];

const byStatus = groupBy(orders, (o) => o.status);
//    ^? Partial<Record<"paid" | "pending" | "refunded", Order[]>>

const paidTotal = (byStatus.paid ?? []).reduce((sum, o) => sum + o.total, 0); // 125
const refunds = byStatus.refunded?.length ?? 0;                              // 0

const bySize = groupByToMap(orders, (o) => (o.total >= 50 ? "large" : "small"));
console.log(bySize.get("large")); // [{ id: 3, ... }]