Proyek: Generic Utils
Bangun mini-library fungsi generik: groupBy, unique, dan pickKeys. Tugas dinilai AI.
Misi: Mini-Lodash yang Type-Safe
Library seperti lodash punya ratusan fungsi utilitas, tapi versi JavaScript-nya longgar secara tipe. Di proyek ini kamu membangun tiga utilitas generik sendiri: unique, groupBy, dan pickKeys. Kita kerjakan langkah demi langkah, tiap langkah dengan penjelasan kenapa generic-nya ditulis seperti itu. Di akhir kamu punya file utilitas yang bisa dipakai ulang di project mana pun, plus pemahaman mendalam tentang cara generic "mengalir".
Langkah 1: unique, Generic Paling Sederhana
Mulai dari yang paling mudah: buang duplikat dari array, pertahankan urutan. Generic di sini hanya meneruskan tipe elemen:
function unique<T>(arr: T[]): T[] {
return [...new Set(arr)];
}
const a = unique([1, 2, 2, 3, 1]); // number[]
const b = unique(["x", "y", "x"]); // string[]
const c = unique([{ id: 1 }, { id: 1 }]); // { id: number }[]Kenapa T penting di sini? Tanpa generic, new Set(arr) pada array campuran akan menghasilkan tipe yang melebar dan informasi spesifik hilang. Dengan T, tipe elemen input sama persis dengan tipe elemen output. Catatan: untuk object, Set membandingkan referensi, bukan isi, jadi dua object "kembar" tetap dianggap beda. Itu perilaku JavaScript, bukan bug generic-mu.
Langkah 2: groupBy, Generic + Record
Sekarang naik level: kelompokkan array menjadi object berdasarkan fungsi kunci. Di sini generic bertemu Record:
function groupBy<T>(arr: T[], kunci: (item: T) => string): Record<string, T[]> {
const hasil: Record<string, T[]> = {};
for (const item of arr) {
const k = kunci(item);
(hasil[k] ??= []).push(item);
}
return hasil;
}
interface Karyawan { nama: string; divisi: string; }
const tim: Karyawan[] = [
{ nama: "Ayu", divisi: "IT" },
{ nama: "Bimo", divisi: "HR" },
{ nama: "Citra", divisi: "IT" },
];
const perDivisi = groupBy(tim, (k) => k.divisi);
// perDivisi: Record<string, Karyawan[]>
console.log(perDivisi["IT"]); // Karyawan[], autocomplete nama dan divisi jalanPerhatikan alurnya: T di-infer sebagai Karyawan dari argumen pertama, lalu parameter kedua kunci otomatis tahu bahwa item-nya Karyawan, sehingga k.divisi ter-type. Inilah "aliran" generic: satu inference di awal menghidupkan type safety di seluruh signature. Return type Record<string, T[]> memberi tahu pemakai bahwa hasilnya kamus berisi array, bukan array biasa.
Sintaks (hasil[k] ??= []) artinya "kalau belum ada array untuk kunci ini, buat dulu". Ini operator logical assignment, cara ringkas yang idiomatis.
Langkah 3: pickKeys, Dua Generic + Constraint
Langkah tersulit dan paling berharga: ambil sebagian properti dari object, dengan key yang dijamin valid oleh compiler:
function pickKeys<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const hasil = {} as Pick<T, K>;
for (const k of keys) {
hasil[k] = obj[k];
}
return hasil;
}
const produk = { id: "p1", nama: "Kopi", harga: 25000, stok: 100 };
const ringkas = pickKeys(produk, ["id", "nama", "harga"]);
// ringkas: Pick<{...}, "id" | "nama" | "harga">
// = { id: string; nama: string; harga: number }
console.log(ringkas.harga.toFixed(0)); // number, amanAda tiga hal yang bekerja sama di sini. Pertama, T menangkap tipe object lengkap. Kedua, K extends keyof T membatasi array key hanya ke key yang benar-benar ada, sekaligus meng-infer union literal key-nya ("id" | "nama" | "harga"). Ketiga, Pick<T, K> membangun tipe hasil secara otomatis dari dua informasi itu. Coba langgar aturannya dan lihat compiler menjaga:
const gagal = pickKeys(produk, ["id", "warna"]);
// Error: Type '"warna"' is not assignable to type
// '"id" | "nama" | "harga" | "stok"'.Typo key ditolak sebelum kode jalan. Inilah bedanya dengan versi any: pickKeys(obj as any, keys as string[]) akan lolos compile lalu me-return undefined diam-diam saat runtime.
Langkah 4: Uji Type Safety End-to-End
Gabungkan ketiganya dalam skenario mini yang mirip project nyata, misal rekap transaksi:
interface Transaksi { id: string; kasir: string; total: number; }
const data: Transaksi[] = [
{ id: "t1", kasir: "Ayu", total: 50000 },
{ id: "t2", kasir: "Bimo", total: 30000 },
{ id: "t3", kasir: "Ayu", total: 25000 },
];
const perKasir = groupBy(data, (t) => t.kasir);
const namaKasir = unique(data.map((t) => t.kasir)); // string[]
const ringkasan = perKasir["Ayu"].map((t) => pickKeys(t, ["id", "total"]));
// ringkasan: Pick<Transaksi, "id" | "total">[]
console.log(namaKasir, ringkasan);Setiap tahap ter-type: perKasir["Ayu"] adalah Transaksi[], ringkasan hanya punya id dan total. Kalau besok interface Transaksi tambah field metode, tidak ada satu pun signature yang perlu diubah.
Catatan teknis: Pola
pickKeys(dua generic +keyofconstraint +Pickdi return) adalah pola yang dipakai library typed besar. Setelah menguasainya, kamu bisa membaca source code library seperti tRPC atau Zod versi typed tanpa tersesat, karena mereka memakai komposisi generic yang sama, hanya lebih berlapis.
Tantangan
Mulai dari groupBy
Implementasikan dulu groupBy<T> saja sebagai pemanasan. Loop array, panggil fungsi kunci untuk tiap item, tampung ke object hasil. Return type-nya Record<string, T[]>.
function groupBy<T>(arr: T[], kunci: (item: T) => string): Record<string, T[]> {
const hasil: Record<string, T[]> = {};
// loop dan kelompokkan
return hasil;
}Tugas
Mini-Library Generic Utils
Implementasikan ketiga fungsi generik berikut dalam satu file:
groupBy<T>(arr: T[], kunci: (item: T) => string): Record<string, T[]>unique<T>(arr: T[]): T[](pakai Set, pertahankan urutan)pickKeys<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>
Sertakan contoh pemakaian tiap fungsi dengan console.log. Semua harus lolos tsc tanpa error.
Kriteria penilaian:
- Ketiga fungsi memakai generic dengan benar (bukan any)
- Return type eksplisit dan akurat
- pickKeys memakai constraint K extends keyof T
- Ada contoh pemakaian yang menunjukkan type safety
AI tutor akan memeriksa bug, kesalahan syntax, dan memberi saran perbaikan.
Kuis Bab
Uji pemahamanmu: Generic
Jawab 5 soal berikut, lalu tekan "Periksa Jawaban".
1.Apa masalah yang diselesaikan generic <T>?
2.Apa itu constraint T extends { id: string }?
3.Apa beda Partial<User> dan Pick<User, "nama">?
4.Apa arti Record<string, number>?
5.Kapan default type parameter <T = string> dipakai?