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[]>>.
/**
* 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:Tialah jenis item (diinferens daripada array anda) danKialah jenis kunci.PropertyKeybermaksudstring | number | symbol— perkara yang boleh menjadi kunci objek.readonly T[]berjanji kita tidak akan mengubah array anda, dan membolehkan anda menghantar array dan tuplereadonlyjuga.- Jika
getKeymemulangkan"paid" | "pending", makaKtepat union itu, jadigroups.paidboleh autocomplete dangroups.paiddmenjadi ralat jenis. Partial<Record<K, T[]>>mengakui bahawa sesuatu kumpulan mungkin tiada (tiada pesanan pending hari ini), jadi TypeScript memaksa anda uruskanundefined.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 kirinullatauundefined.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
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, ... }]