Generics Lanjutan: Constraint, Interface & Class Generik
Membatasi parameter tipe dengan extends, memakai keyof sebagai batasan, nilai default parameter tipe, serta membuat interface dan class generik seperti Tumpukan (Stack).
Constraint: membatasi T dengan extends
Kadang T terlalu bebas. Jika fungsi butuh properti length, kita harus menyatakan bahwa T minimal punya length:
function terpanjang<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b
}
terpanjang("kucing", "gajah") // ✅ string punya length → "kucing"
terpanjang([1, 2], [1, 2, 3]) // ✅ array punya length
terpanjang(10, 20) // ❌ Argument of type 'number' is not assignable to parameter of type '{ length: number }'
T extends X dibaca "T harus kompatibel dengan X". Hasilnya tetap T (bukan sekadar { length: number }), jadi tipe asli tidak hilang.
keyof sebagai batasan
Pola paling berguna: mengambil properti object dengan kunci yang dijamin ada:
function ambil<T, K extends keyof T>(obj: T, kunci: K): T[K] {
return obj[kunci]
}
const pengguna = { id: 1, nama: "Ani", aktif: true }
const n = ambil(pengguna, "nama") // string
const i = ambil(pengguna, "id") // number
ambil(pengguna, "email") // ❌ Argument of type '"email"' is not assignable to parameter of type '"id" | "nama" | "aktif"'
Di sini keyof T menghasilkan gabungan semua nama properti T, dan T[K] adalah tipe nilai pada kunci itu. Keduanya dibahas penuh di bab 16 — yang penting kamu sudah melihat gunanya: autocomplete kunci + tipe hasil yang tepat.
Interface dan type generik
interface Respons<T> {
sukses: boolean
data: T
pesan?: string
}
type DaftarProduk = Respons<{ id: number; nama: string }[]>
const r: Respons<string> = { sukses: true, data: "OK" }
Nilai default untuk parameter tipe
interface Halaman<T = string> {
isi: T[]
nomor: number
}
const a: Halaman = { isi: ["a"], nomor: 1 } // T = string (default)
const b: Halaman<number> = { isi: [1, 2], nomor: 1 } // T = number
Class generik: contoh Tumpukan (Stack)
Struktur data adalah rumah alami generics: logikanya sama, isinya berbeda-beda.
class Tumpukan<T> {
private data: T[] = []
push(item: T): void { this.data.push(item) }
pop(): T | undefined { return this.data.pop() }
lihat(): T | undefined { return this.data[this.data.length - 1] }
get ukuran(): number { return this.data.length }
}
const angka = new Tumpukan<number>()
angka.push(1)
angka.push(2)
angka.push("tiga") // ❌ hanya number
angka.pop() // 2 (tipe: number | undefined)
const teks = new Tumpukan<string>() // satu class, tipe berbeda
Constraint pada class dan beberapa batasan
interface PunyaId { id: number }
class Repositori<T extends PunyaId> {
private item = new Map<number, T>()
simpan(x: T): void { this.item.set(x.id, x) }
cari(id: number): T | undefined { return this.item.get(id) }
}
const repo = new Repositori<{ id: number; nama: string }>()
repo.simpan({ id: 1, nama: "Ani" })
repo.cari(1)?.nama
Pola Repositori<T> ini sangat umum di aplikasi nyata: satu class untuk menyimpan/menemukan data apa pun yang punya id.
Fungsi generik dengan callback
function kelompokkan<T, K extends string>(daftar: T[], kunci: (x: T) => K): Record<K, T[]> {
const hasil = {} as Record<K, T[]>
for (const x of daftar) {
const k = kunci(x)
;(hasil[k] ??= []).push(x)
}
return hasil
}
(Jangan khawatir bila baris ini terasa padat — kamu akan terbiasa. Perhatikan bagaimana T dan K saling berhubungan.)
Kesalahan umum pemula
- Lupa constraint lalu mengakses properti pada
T(error: "Property 'length' does not exist on type 'T'"). - Menulis constraint yang terlalu sempit sehingga fungsi tak bisa dipakai oleh tipe yang wajar.
- Memakai
asuntuk memaksa tipe generik padahal bisa diselesaikan dengan constraint yang benar.