genericclassoopMenengah4 mnt baca

Generic pada Class

Class dengan parameter tipe: pola Stack<T>, repository, dan container generik.

Kenapa Class Butuh Generic

Class adalah cetakan object yang hidup lama: ia menyimpan state di field dan field itu dipakai lintas method. Kalau tipenya di-hardcode, kamu harus menulis ulang seluruh class untuk tiap tipe data. Generic membuat satu class bisa "dikunci" ke tipe apa pun saat di-instantiate. Analoginya seperti kontainer pengiriman: bentuk kotaknya sama, tapi sekali disegel dengan label "elektronik", isinya tidak boleh tiba-tiba jadi "sayuran".

Contoh Klasik: Stack<T>

typescript
class Tumpukan<T> {
  private items: T[] = [];

  dorong(item: T): void {
    this.items.push(item);
  }

  ambil(): T | undefined {
    return this.items.pop();
  }

  get kosong(): boolean {
    return this.items.length === 0;
  }
}

const riwayat = new Tumpukan<string>();
riwayat.dorong("/beranda");
riwayat.dorong("/profil");
const halaman = riwayat.ambil(); // string | undefined
riwayat.dorong(42);
// Error: Argument of type 'number' is not assignable to parameter of type 'string'.

Tipe dikunci sekali di new Tumpukan<string>(), lalu seluruh method (dorong, ambil, getter) otomatis mengikuti. Satu class, dipakai untuk riwayat navigasi (string), undo stack editor (object perintah), atau tumpukan angka kalkulator.

Skenario Nyata: Typed Event Emitter

Di aplikasi frontend, event bus yang type-safe menyelamatkan banyak bug. Tanpa generic, payload event biasanya any dan typo nama event baru ketahuan saat diklik user:

typescript
type EventMap = {
  login: { userId: string };
  logout: void;
  "keranjang:update": { jumlah: number };
};

class EventBus<TEvents extends Record<string, any>> {
  private handlers = new Map<string, Function[]>();

  on<K extends keyof TEvents>(
    event: K,
    handler: (payload: TEvents[K]) => void
  ): void {
    const list = this.handlers.get(event as string) ?? [];
    list.push(handler);
    this.handlers.set(event as string, list);
  }

  emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {
    this.handlers.get(event as string)?.forEach((h) => h(payload));
  }
}

const bus = new EventBus<EventMap>();
bus.on("login", (p) => console.log(p.userId)); // p ter-type otomatis
bus.emit("login", { userId: "u1" }); // OK
bus.emit("login", { salah: 1 });
// Error: Argument of type '{ salah: number; }' is not assignable
// to parameter of type '{ userId: string; }'.

Satu class generic menjadi tulang punggung komunikasi antar komponen, dengan nama event dan bentuk payload yang diperiksa compiler. Ini pola yang dipakai library seperti mitt versi typed dan banyak state manager.

Contoh Error Nyata: T Tidak Ter-infer

Tidak seperti fungsi, class tidak selalu bisa meng-infer T dari constructor. Kalau constructor tidak menerima argumen bertipe T, kamu wajib menulis eksplisit:

typescript
class Wadah<T> {
  private nilai?: T;
  isi(v: T): void { this.nilai = v; }
  baca(): T | undefined { return this.nilai; }
}

const w = new Wadah();
w.isi("halo");
const x: number = w.baca() ?? 0;
// Error: Type 'unknown' is not assignable to type 'number'.

Karena tidak ada petunjuk, T jatuh ke unknown dan semuanya jadi seret. Perbaikannya satu kata: new Wadah<string>(). Aturan mainnya sederhana: kalau constructor menerima T, inference jalan; kalau tidak, tulis eksplisit.

Generic Class dengan Constraint: Repository

Pola favorit backend: satu class repository untuk semua entity yang punya id.

typescript
interface Entity { id: string; }

class Repository<T extends Entity> {
  private data = new Map<string, T>();

  simpan(item: T): void {
    this.data.set(item.id, item);
  }

  cari(id: string): T | undefined {
    return this.data.get(id);
  }

  semua(): T[] {
    return [...this.data.values()];
  }
}

interface User extends Entity { nama: string; }
const users = new Repository<User>();
users.simpan({ id: "1", nama: "Budi" }); // OK
users.simpan({ nama: "Tanpa ID" });
// Error: Argument of type '{ nama: string; }' is not assignable to
// parameter of type 'User'. Property 'id' is missing.

Constraint T extends Entity menjamin item.id selalu ada di dalam method simpan, sekaligus menolak entity yang tidak punya id saat dipakai. Satu class melayani Repository<User>, Repository<Produk>, Repository<Transaksi> tanpa satu baris duplikasi.

Catatan teknis: Field private di class generic tetap type-safe penuh: private items: T[] hanya bisa diakses dari dalam class, dan subclass generic mewarisi parameter tipenya. Hindari pola "generic di method tapi class-nya tidak" untuk state yang disimpan lama, karena tipenya bisa berubah tiap pemanggilan dan merusak konsistensi data.

Tantangan

Antrian generik

Buat class Antrian<T> dengan masuk(item: T): void, keluar(): T | undefined (FIFO pakai shift), dan jumlah(): number. Test dengan Antrian<number>: masukkan 1, 2, 3, keluarkan satu, cetak jumlah sisa.

typescript
class Antrian<T> {
  private items: T[] = [];

  masuk(item: T): void {
    // ...
  }

  keluar(): T | undefined {
    // ...
  }

  jumlah(): number {
    // ...
  }
}

const q = new Antrian<number>();
q.masuk(1); q.masuk(2); q.masuk(3);
console.log(q.keluar()); // 1
console.log(q.jumlah()); // 2