Kenapa ini penting
Coba bayangkan kamu menulis function ambilPertama yang mengembalikan elemen pertama array. Untuk array angka, hasilnya angka. Untuk array string, hasilnya string. Untuk array Produk, hasilnya Produk. Tanpa generics, pilihanmu cuma dua: menulis tiga function yang isinya sama, atau memakai any dan kehilangan semua informasi tipe. Generics adalah jalan ketiga: satu function yang "mengingat" tipe yang dimasukkan dan mengembalikannya dengan tepat.
Generics ada di mana-mana walaupun mungkin belum kamu sadari: Array<string>, Promise<User>, useState<number>(), useQuery<Produk[]>(), z.infer<typeof skema>. Begitu kamu paham generics, membaca tipe library seperti React, TanStack Query, dan Zod jauh lebih mudah. Analoginya seperti cetakan kue: cetakannya satu, adonannya bisa cokelat, pandan, atau keju. Kue yang keluar selalu sesuai adonan yang dimasukkan.
Konsep inti
Function generic
function ambilPertama<T>(daftar: T[]): T | undefined {
return daftar[0];
}
const angka = ambilPertama([10, 20, 30]); // T = number → number | undefined
const kata = ambilPertama(["apel", "jeruk"]); // T = string → string | undefined
<T> adalah parameter tipe. Seperti parameter function biasa yang menerima nilai, parameter tipe menerima tipe. Biasanya kamu tidak perlu menulis ambilPertama<number>(...) karena TypeScript menyimpulkan T dari argumen. Nama T hanya konvensi; untuk kode yang lebih jelas boleh pakai nama bermakna seperti TItem atau TData.
Beberapa parameter tipe
function pasangkan<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const pasangan = pasangkan("Sari", 25); // [string, number]
Interface dan type generic
interface ResponsApi<T> {
data: T;
status: number;
pesan: string;
}
type Halaman<T> = {
items: T[];
halaman: number;
totalHalaman: number;
};
const responsUser: ResponsApi<{ id: number; nama: string }> = {
data: { id: 1, nama: "Sari" },
status: 200,
pesan: "OK",
};
Satu bentuk ResponsApi bisa dipakai untuk user, produk, atau apa pun. Ini pola yang sangat umum saat membungkus respons backend.
Constraint dengan extends
Kadang T tidak boleh sembarang tipe. Misalnya function yang membaca .length hanya masuk akal untuk tipe yang punya length:
function terpanjang<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
terpanjang("kucing", "gajah"); // OK, string punya length
terpanjang([1, 2], [1, 2, 3]); // OK, array punya length
T extends X berarti "T boleh tipe apa saja asalkan memenuhi X". Constraint yang sangat sering dipakai adalah K extends keyof T: kunci yang benar-benar ada di object T.
function ambilNilai<T, K extends keyof T>(obj: T, kunci: K): T[K] {
return obj[kunci];
}
keyof dan T[K] (indexed access) akan dibahas lebih dalam di modul Utility Types.
Default type parameter
Seperti parameter function yang punya nilai default, parameter tipe juga bisa:
type Respons<TData = unknown, TError = string> = {
data?: TData;
error?: TError;
};
const r1: Respons = {}; // TData = unknown, TError = string
const r2: Respons<number[]> = { data: [1, 2] }; // TError tetap string
Generics di class
class Antrian<T> {
private isi: T[] = [];
masuk(item: T): void { this.isi.push(item); }
keluar(): T | undefined { return this.isi.shift(); }
}
Membaca tipe generic milik library
Setelah paham <T>, signature library yang dulu terlihat menakutkan jadi bisa dibaca. Beberapa contoh yang sudah sering kamu pakai:
| Yang kamu tulis | Signature (disederhanakan) | Artinya |
|---|---|---|
[1, 2].map((n) => String(n)) |
map<U>(fn: (value: T) => U): U[] |
Tipe hasil U disimpulkan dari callback, jadi hasilnya string[] |
useState(0) |
useState<S>(initial: S): [S, Dispatch<SetStateAction<S>>] |
S disimpulkan dari nilai awal, jadi number |
fetch(url) |
fetch(...): Promise<Response> |
Promise adalah generic: isi yang akan datang adalah Response |
new Map<string, number>() |
Map<K, V> |
Dua parameter tipe: kunci dan nilai |
Pola membacanya selalu sama: cari huruf di dalam < >, lalu lihat di mana huruf itu muncul lagi (parameter dan nilai kembalian). Tempat munculnya itulah yang "terhubung".
Kapan tidak perlu generics
Kalau parameter tipe hanya dipakai satu kali (misalnya function cetak<T>(x: T): void), generics tidak memberi apa-apa; cukup unknown. Generics berguna ketika ia menghubungkan dua hal: input dengan output, atau dua parameter satu sama lain.
Contoh kode
// generics.ts
// 1. Function generic + constraint
function kelompokkan<T, K extends string>(daftar: T[], ambilKunci: (item: T) => K): Record<K, T[]> {
const hasil = {} as Record<K, T[]>;
for (const item of daftar) {
const kunci = ambilKunci(item);
(hasil[kunci] ??= []).push(item);
}
return hasil;
}
type Produk = { nama: string; kategori: "makanan" | "minuman"; harga: number };
const menu: Produk[] = [
{ nama: "Nasi Goreng", kategori: "makanan", harga: 25000 },
{ nama: "Es Teh", kategori: "minuman", harga: 5000 },
{ nama: "Mie Ayam", kategori: "makanan", harga: 20000 },
];
const perKategori = kelompokkan(menu, (p) => p.kategori);
// tipe: Record<"makanan" | "minuman", Produk[]>
console.log(perKategori.makanan.map((p) => p.nama)); // → [ 'Nasi Goreng', 'Mie Ayam' ]
console.log(perKategori.minuman.length); // → 1
// 2. keyof constraint
function ambilNilai<T, K extends keyof T>(obj: T, kunci: K): T[K] {
return obj[kunci];
}
const hargaPertama = ambilNilai(menu[0]!, "harga"); // tipe: number
console.log(hargaPertama); // → 25000
ambilNilai(menu[0]!, "stok"); // ❌ error TS2345: Argument of type '"stok"' is not assignable to parameter of type 'keyof Produk'.
// 3. Interface generic untuk respons API
interface ResponsApi<T> {
ok: boolean;
data: T;
}
function bungkus<T>(data: T): ResponsApi<T> {
return { ok: true, data };
}
const respons = bungkus(menu); // ResponsApi<Produk[]>
console.log(respons.data[1]?.nama); // → Es Teh
// 4. Constraint yang tidak terpenuhi
function terpanjang<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(terpanjang("kucing", "gajah")); // → kucing
terpanjang(10, 20); // ❌ error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
Catatan: tanda ! pada menu[0]! adalah non-null assertion ("saya yakin ini tidak undefined"). Kita memakainya karena tsconfig latihan menyalakan noUncheckedIndexedAccess, sehingga menu[0] bertipe Produk | undefined. Pakai ! hanya kalau kamu benar-benar yakin.
Contoh nyata: custom hook useFetch<T>
Inilah generics yang akan sering kamu tulis di React. Hook ini tidak tahu data apa yang diambil; pemanggil yang menentukan lewat T.
// useFetch.tsx
import { useEffect, useState } from "react";
type StatusFetch<T> =
| { status: "memuat" }
| { status: "sukses"; data: T }
| { status: "gagal"; pesan: string };
export function useFetch<T>(url: string): StatusFetch<T> {
const [state, setState] = useState<StatusFetch<T>>({ status: "memuat" });
useEffect(() => {
const controller = new AbortController();
setState({ status: "memuat" });
fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>; // asumsi bentuk data; modul Data API membahas validasinya
})
.then((data) => setState({ status: "sukses", data }))
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setState({ status: "gagal", pesan: err instanceof Error ? err.message : "Gagal memuat" });
});
return () => controller.abort();
}, [url]);
return state;
}
// Pemakaian
type Todo = { id: number; title: string; completed: boolean };
export function DaftarTodo() {
const hasil = useFetch<Todo[]>("https://jsonplaceholder.typicode.com/todos?_limit=5");
if (hasil.status === "memuat") return <p>Memuat...</p>;
if (hasil.status === "gagal") return <p>Error: {hasil.pesan}</p>;
return (
<ul>
{hasil.data.map((todo) => (
<li key={todo.id}>{todo.completed ? "✅" : "⬜"} {todo.title}</li>
))}
</ul>
);
}
Di dalam hasil.data.map, editor tahu todo punya id, title, dan completed. Kalau kamu salah ketik todo.tittle, error langsung muncul. Itu semua karena satu <Todo[]> saat memanggil hook.
Kesalahan umum
- Memakai
anypadahal butuh generics.function pertama(arr: any[]): anymembuang informasi tipe. Dengan<T>, tipe mengalir dari input ke output. - Generics yang dipakai sekali saja.
function log<T>(x: T): voidtidak lebih baik daripadax: unknown. Generics berguna jika menghubungkan beberapa posisi. - Lupa constraint lalu bingung kenapa
.lengthatau.idditolak. Kalau function butuh properti tertentu, nyatakan denganT extends { id: number }. - Menulis argumen tipe secara manual padahal bisa disimpulkan.
ambilPertama<number>([1, 2])tidak salah tapi berisik. Tulis manual hanya kalau TypeScript tidak bisa menebak, misalnyauseState<User | null>(null). - Mengira
useFetch<T>memvalidasi data.Thanya janji di compile time. Kalau API mengirim bentuk lain, TypeScript tidak tahu. Solusinya ada di modul Data API (Zod).
Latihan
- Mudah, Tulis
ambilTerakhir<T>(daftar: T[]): T | undefineddan uji dengan array angka dan array string. - Sedang, Tulis
type Hasil<T, E = string> = { ok: true; nilai: T } | { ok: false; error: E }lalu functionbagiAman(a: number, b: number): Hasil<number>yang gagal kalaub === 0. - Menantang, Tulis
urutkanBerdasarkan<T, K extends keyof T>(daftar: T[], kunci: K): T[]yang mengembalikan array baru terurut naik berdasarkan propertikunci(angka atau string).
Lihat solusi
// 1. Mudah
function ambilTerakhir<T>(daftar: T[]): T | undefined {
return daftar[daftar.length - 1];
}
console.log(ambilTerakhir([1, 2, 3])); // → 3
console.log(ambilTerakhir(["a", "b", "c"])); // → c
// 2. Sedang
type Hasil<T, E = string> = { ok: true; nilai: T } | { ok: false; error: E };
function bagiAman(a: number, b: number): Hasil<number> {
if (b === 0) return { ok: false, error: "Tidak bisa membagi dengan nol" };
return { ok: true, nilai: a / b };
}
const h = bagiAman(10, 0);
console.log(h.ok ? h.nilai : h.error); // → Tidak bisa membagi dengan nol
// 3. Menantang
function urutkanBerdasarkan<T, K extends keyof T>(daftar: T[], kunci: K): T[] {
return [...daftar].sort((a, b) => {
const x = a[kunci];
const y = b[kunci];
if (x < y) return -1;
if (x > y) return 1;
return 0;
});
}
const siswa = [
{ nama: "Citra", nilai: 90 },
{ nama: "Adi", nilai: 75 },
{ nama: "Bayu", nilai: 82 },
];
console.log(urutkanBerdasarkan(siswa, "nilai").map((s) => s.nama)); // → [ 'Adi', 'Bayu', 'Citra' ]
console.log(urutkanBerdasarkan(siswa, "nama").map((s) => s.nama)); // → [ 'Adi', 'Bayu', 'Citra' ]
Ringkasan
- Generics (
<T>) membuat function, interface, type, dan class bekerja untuk banyak tipe sambil tetap menjaga informasi tipe. - TypeScript biasanya menyimpulkan
Tdari argumen; tulis manual hanya kalau perlu. T extends Xmembatasi tipe yang diterima;K extends keyof Tmenjamin kunci benar-benar ada.- Parameter tipe bisa punya default:
type Respons<T = unknown>. - Hook seperti
useFetch<T>adalah pemakaian generics paling umum di React, tapiTtetap hanya janji, bukan validasi.