Generics — TypeScript

Generics memungkinkan fungsi/class bekerja dengan berbagai tipe tanpa kehilangan type safety: // Tanpa generics — harus pakai any (tidak aman) function…

Generic adalah cara menulis satu fungsi (atau satu tipe) yang bisa dipakai untuk banyak tipe tanpa kehilangan informasi tipenya. Caranya dengan type parameter, biasanya bernama T: semacam variabel, tapi isinya tipe, dan ditentukan setiap kali fungsinya dipanggil. Mirip tempat penitipan barang: petugasnya gak peduli kamu titip helm atau payung, tapi yang kamu ambil pasti barang yang sama dengan yang kamu titipkan.

🧪 Tebakan cepat: terakhir([1, 2, 3]) dan terakhir(["a", "b"]) memanggil fungsi yang sama. Apa tipe hasil masing-masing? Tebak dulu, jawabannya ada di bagian "T ikut argumennya".

Kenapa gak pakai any saja

Kita mau fungsi yang mengambil elemen terakhir dari array apa pun. Cara gampangnya pakai any:

function terakhirAny(arr: any[]): any {
  return arr[arr.length - 1];
}

const hasil = terakhirAny([1, 2, 3]); // hasil: any
hasil.toUpperCase(); // TypeScript diam saja
TypeError: hasil.toUpperCase is not a function

any mematikan pengecekan. hasil sebenarnya angka 3, dan angka gak punya .toUpperCase(), tapi TypeScript gak protes. Error-nya baru muncul saat program jalan, padahal TypeScript dipakai justru supaya error seperti ini ketahuan sebelum jalan.

T ikut argumennya

function terakhir<T>(arr: T[]): T {
  return arr[arr.length - 1];
}

const a = terakhir([1, 2, 3]);     // T = number, a: number, nilainya 3
const b = terakhir(["a", "b"]);    // T = string, b: string, nilainya "b"
const c = terakhir([true, false]); // T = boolean, c: boolean, nilainya false
Panggilan T Tipe hasil Nilai
terakhir([1, 2, 3]) number number 3
terakhir(["a", "b"]) string string "b"
terakhir([1, "a"]) string | number string | number "a"

Jadi hasilnya number dan string. Itu jawaban tebakannya. Karena tipenya tepat, b.toUpperCase() boleh, sedangkan a.toUpperCase() ditolak sebelum jalan:

error TS2339: Property 'toUpperCase' does not exist on type 'number'.

T juga boleh kamu tulis sendiri: terakhir<string>(["a", "b"]). Kalau isinya gak cocok, misalnya terakhir<string>([1, 2, 3]), tiap angka ditolak dengan Type 'number' is not assignable to type 'string'. (ts(2322)).

Membatasi T dengan extends

Kadang fungsimu butuh sesuatu dari T. Fungsi panjang di bawah butuh .length:

function panjang<T extends { length: number }>(isi: T): number {
  return isi.length;
}

panjang("halo");    // 4
panjang([1, 2, 3]); // 3
panjang(123);       // error
error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

Tipe yang generic

Bukan cuma fungsi. Interface dan type alias juga bisa punya type parameter:

interface ApiResponse<T> {
  data: T;
  status: number;
}

const res: ApiResponse<string[]> = { data: ["Kopi", "Teh"], status: 200 };

Bentuk ApiResponse sama untuk semua endpoint, cuma tipe data-nya yang beda. Kamu sebenarnya sudah sering memakai tipe generic: string[] sama dengan Array<string>.

⚠️ Jebakan yang sering ketemu

TL;DR: Generic memakai type parameter seperti <T> supaya satu fungsi bisa dipakai untuk banyak tipe tanpa kehilangan tipenya. function terakhir<T>(arr: T[]): T mengembalikan elemen terakhir (arr[arr.length - 1]), dan T disimpulkan dari argumen: terakhir([1, 2, 3]) bertipe number, terakhir(["a", "b"]) bertipe string. Beda dengan any, kesalahan seperti .toUpperCase() pada angka ketahuan sebelum jalan. Batasi T dengan extends, misalnya T extends { length: number }, supaya panjang(123) ditolak. Interface juga bisa generic, seperti ApiResponse<T>.

Yang akan kamu pelajari