Generics: Fungsi yang Fleksibel tapi Tetap Aman
Memahami masalah yang dipecahkan generics, menulis fungsi generik dengan parameter tipe <T>, inference otomatis, lebih dari satu parameter tipe, dan generics pada array.
Masalahnya
Bayangkan fungsi yang mengembalikan elemen pertama sebuah array. Untuk array angka ia mengembalikan angka, untuk array teks mengembalikan teks. Bagaimana menuliskan tipenya?
function pertamaAngka(arr: number[]): number | undefined { return arr[0] }
function pertamaTeks(arr: string[]): string | undefined { return arr[0] }
// ...mengulang untuk setiap tipe? Melelahkan.
function pertamaAny(arr: any[]): any { return arr[0] }
const x = pertamaAny([1, 2, 3]) // x: any β tipe hilang, proteksi hilang π
Kita ingin: "apa pun tipe isi array-nya, kembalikan tipe yang sama". Itulah tugas generics.
Parameter tipe <T>
Generics memperkenalkan variabel untuk tipe, ditulis dalam kurung siku < >. Anggap T sebagai "tipe apa pun yang nanti ditentukan saat fungsi dipakai":
function pertama<T>(arr: T[]): T | undefined {
return arr[0]
}
const a = pertama([1, 2, 3]) // T = number β a: number | undefined
const b = pertama(["x", "y"]) // T = string β b: string | undefined
const c = pertama([true, false]) // T = boolean
Kamu tidak perlu menulis <number> saat memanggil: TypeScript menyimpulkan T dari argumen. Boleh juga menulisnya eksplisit: pertama<number>([1, 2]) β berguna saat inference tidak cukup (mis. array kosong).
Analogi
Generics seperti kotak dengan label yang bisa diganti. Kotaknya sama (logikanya sama), tetapi label "isi: T" diganti sesuai barang yang dimasukkan. Fungsi pertama adalah satu kotak untuk semua jenis barang, dan tipe barang yang keluar selalu sama dengan yang masuk.
Contoh klasik: identity
function identitas<T>(nilai: T): T {
return nilai
}
identitas("halo") // tipe kembalian: "halo" (string literal)
identitas(42) // 42
Lebih dari satu parameter tipe
Beri nama yang berurutan atau bermakna: T, U, K, V, atau nama deskriptif.
function pasangkan<A, B>(a: A, b: B): [A, B] {
return [a, b]
}
const p = pasangkan("umur", 25) // [string, number]
function petakan<Masuk, Keluar>(daftar: Masuk[], fn: (x: Masuk) => Keluar): Keluar[] {
const hasil: Keluar[] = []
for (const x of daftar) hasil.push(fn(x))
return hasil
}
const panjang = petakan(["a", "bb", "ccc"], (s) => s.length) // number[]
Generics di tipe bawaan
Kamu sebenarnya sudah memakai generics tanpa sadar:
| Tipe | Artinya |
|---|---|
Array<string> | Sama dengan string[] |
Promise<number> | Janji yang akan menghasilkan number (bab 21) |
Map<string, number> | Peta dengan kunci string dan nilai number |
Set<string> | Himpunan berisi string |
Record<string, boolean> | Object berkunci string bernilai boolean |
const peta = new Map<string, number>()
peta.set("apel", 3)
peta.set("jeruk", "lima") // β Argument of type 'string' is not assignable to parameter of type 'number'
Generics di type alias dan interface
type Kotak<T> = { isi: T }
const kotakAngka: Kotak<number> = { isi: 7 }
const kotakTeks: Kotak<string> = { isi: "halo" }
interface Hasil<T> {
data: T
waktu: number
}
Kapan memakai generics?
- Saat logikanya sama untuk banyak tipe, dan hubungan antara tipe masukan dan keluaran perlu dijaga.
- Kalau tipenya hanya dipakai sekali di tanda tangan fungsi, biasanya generics tidak perlu:
function cetak(x: string[])lebih sederhana daripadacetak<T extends string>(x: T[]).
Kesalahan umum pemula
- Memakai
anypadahal generics bisa mempertahankan tipe. - Membuat parameter tipe yang tidak dipakai di mana pun selain satu tempat (tanpa hubungan antar tipe).
- Memakai nama tipe satu huruf tanpa makna untuk situasi rumit β gunakan nama deskriptif seperti
TItem.