Lompat ke konten
Kembali ke modul

TypeScript: Menangkap Kesalahan Sebelum Sampai Produksi · 5/8

Generic tanpa rasa takut

Type yang diisi oleh pemanggilnya. Mulai dari masalah yang diselesaikannya, bukan dari sintaksnya.

Baca 30 menit

Setelah pelajaran ini kamu bisa

  • Menjelaskan masalah yang diselesaikan generic
  • Membaca dan menulis function generic
  • Membatasi type parameter dengan `extends`
  • Mengenali kapan generic tidak diperlukan

Generic tampak menakutkan karena biasanya diajarkan sebagai sintaks. Mulailah dari masalahnya: kamu punya function yang bekerja dengan type apa pun, dan kamu ingin tetap mengetahui type *mana* yang dipakai pemanggilnya.

Masalahnya

ts
// A function that returns the first item of an array.
// Typed with any: it works, and you lose everything.
function firstAny(items: any[]): any {
  return items[0];
}

const name = firstAny(["Ana", "Budi"]);
name.toUpperCase();     // no error - but also no checking
name.toFixed(2);        // ALSO no error. It is a string. This will crash.

// Typed with unknown: safe, but now unusable without a check every time.
function firstUnknown(items: unknown[]): unknown {
  return items[0];
}
const n = firstUnknown(["Ana"]);
n.toUpperCase();        // Error: 'n' is of type 'unknown'
any melepaskan keamanan. unknown menjaga keamanan tapi membuang informasi yang sudah dimiliki pemanggilnya.

Solusinya

Generic adalah type yang diisi oleh pemanggilnya. Tulis <T> untuk menyatakan "ada sebuah type di sini; saya akan menyebutnya T", lalu pakai T di signature-nya. TypeScript menyimpulkan apa itu T di setiap tempat pemanggilan.

ts
function first<T>(items: T[]): T | undefined {
  return items[0];
}

const name = first(["Ana", "Budi"]);   // T is string  -> string | undefined
const count = first([1, 2, 3]);        // T is number  -> number | undefined
const venue = first(venues);           // T is Venue   -> Venue | undefined

name?.toUpperCase();    // fine
name?.toFixed(2);       // Error: 'toFixed' does not exist on type 'string'
Satu function, pemeriksaan lengkap di setiap tempat pemanggilan, dan pemanggilnya tidak perlu menulis anotasi type.

Constraint

T yang polos bisa jadi apa saja, jadi kamu tidak bisa menyentuh property-nya. extends mempersempit apa saja yang boleh menjadi T, sehingga kamu bisa memakainya.

ts
// Without a constraint: T might be a number, which has no .length
function longest<T>(items: T[]) {
  return items.sort((a, b) => b.length - a.length)[0];
  //                            ~~~~~~
  // Property 'length' does not exist on type 'T'.
}

// With a constraint: T must have a length, whatever else it is.
function longest<T extends { length: number }>(items: T[]) {
  return [...items].sort((a, b) => b.length - a.length)[0];
}

longest(["ab", "abcd"]);           // fine - strings have length
longest([[1], [1, 2]]);            // fine - arrays have length
longest([1, 2]);                   // Error: number has no 'length'

// A very common pair: K must be a key that actually exists on T.
function pluck<T, K extends keyof T>(item: T, key: K): T[K] {
  return item[key];
}

const venue = { id: 1, name: "Villa" };
pluck(venue, "name");      // returns string
pluck(venue, "id");        // returns number
pluck(venue, "nope");      // Error: '"nope"' is not assignable to 'id' | 'name'
Coba sendiri

Generic hilang sepenuhnya saat runtime. Ini `pluck` yang sama dalam JS biasa — perilaku identik, tanpa keamanan sama sekali.

Hasil

Tekan Jalankan untuk melihat hasilnya.

Ini jalan di browser kamu, di dalam sandbox. Apa pun yang kamu tulis di sini tidak bisa merusak situs.

Kapan sebaiknya tidak dipakai

Generic adalah alat untuk function yang benar-benar tidak peduli type. Memakainya supaya tampak canggih membuat kode jauh lebih sulit dibaca tanpa manfaat apa pun.

ts
// Pointless: T is used exactly once, so it constrains nothing.
function log<T>(value: T): void {
  console.log(value);
}
// Just write:
function log(value: unknown): void {
  console.log(value);
}

// Pointless: this only ever handles bookings. Say so.
function totalFor<T extends Booking>(booking: T): number {
  return booking.nights * booking.pricePerNight;
}
// Just write:
function totalFor(booking: Booking): number {
  return booking.nights * booking.pricePerNight;
}
Ujinya: apakah type parameter-nya muncul di lebih dari satu tempat, menghubungkan sebuah input ke sebuah output? Kalau tidak, kamu tidak membutuhkannya.

Tugas praktik

Di scratch.ts, tulis groupBy<T, K extends keyof T>(items: T[], key: K) yang mengembalikan object yang memetakan setiap nilai berbeda dari key itu ke array item yang cocok. Uji pada [{role:"lead",name:"A"},{role:"learner",name:"B"},{role:"learner",name:"C"}] dengan "role", dan pastikan compiler menolak "nope" sebagai key-nya.

Hasil yang diharapkan

{ lead: [satu item], learner: [dua item] }, dan error compile untuk "nope".

Tampilkan contoh solusi

Coba dulu minimal sepuluh menit sebelum membuka ini. Bagian yang terasa susah itu justru yang mengajari kamu.

// The plain-JavaScript logic, which is all the runner can execute:
function groupBy(items, key) {
  const out = {};
  for (const item of items) {
    const value = String(item[key]);
    if (!out[value]) out[value] = [];
    out[value].push(item);
  }
  return out;
}

const people = [
  { role: "lead", name: "A" },
  { role: "learner", name: "B" },
  { role: "learner", name: "C" },
];
console.log(groupBy(people, "role"));

// In TypeScript the signature is:
//   function groupBy<T, K extends keyof T>(
//     items: T[], key: K
//   ): Record<string, T[]>
// and groupBy(people, "nope") then fails to compile.