Generics dan class adalah dua fitur TypeScript yang membuat kode benar-benar scalable. Generics memungkinkan fungsi dan class bekerja dengan berbagai tipe sekaligus tetap type-safe — seperti template di C++ atau generics di Java/C#. Class di TypeScript menambahkan access modifiers dan typing di atas class JavaScript biasa. Di artikel ini kita menyatukan semua yang telah dipelajari sebelumnya.
Masalah Tanpa Generics
// Tanpa generics — harus buat fungsi terpisah untuk setiap tipe
function ambil_pertama_number(arr: number[]): number | undefined {
return arr[0];
}
function ambil_pertama_string(arr: string[]): string | undefined {
return arr[0];
}
// Atau pakai any — kehilangan type safety
function ambil_pertama_any(arr: any[]): any {
return arr[0]; // Tipe informasi hilang!
}
Generics — Solusinya
// Satu fungsi untuk semua tipe, tetap type-safe
function ambil_pertama<T>(arr: T[]): T | undefined {
return arr[0];
}
const angka = ambil_pertama([1, 2, 3]); // TypeScript tahu: number | undefined
const kata = ambil_pertama(["a", "b", "c"]); // TypeScript tahu: string | undefined
const mixed = ambil_pertama<boolean>([true, false]); // Eksplisit tipe
console.log(angka); // Output: 1
console.log(kata); // Output: a
// TypeScript mencegah operasi yang salah
// angka.toUpperCase(); // ❌ Error: 'number' does not have 'toUpperCase'
// kata.toFixed(2); // ❌ Error: 'string' does not have 'toFixed'
Generic Functions
// Fungsi pasangan (tuple)
function buat_pasangan<K, V>(kunci: K, nilai: V): [K, V] {
return [kunci, nilai];
}
const p1 = buat_pasangan("nama", "Ali"); // [string, string]
const p2 = buat_pasangan(1, { aktif: true }); // [number, { aktif: boolean }]
// Fungsi filter generic
function saring<T>(items: T[], predikat: (item: T) => boolean): T[] {
return items.filter(predikat);
}
const angka = [1, 2, 3, 4, 5, 6];
const genap = saring(angka, n => n % 2 === 0);
console.log(genap); // Output: [2, 4, 6]
const nama_nama = ["Ali", "Budi", "Candra", "Dewi"];
const nama_panjang = saring(nama_nama, n => n.length > 4);
console.log(nama_panjang); // Output: ['Candra']
Generic Constraints dengan extends
// Constraint: T harus memiliki property 'length'
function tampilkan_panjang<T extends { length: number }>(item: T): string {
return `Panjang: ${item.length}`;
}
console.log(tampilkan_panjang("Halo")); // Output: Panjang: 4
console.log(tampilkan_panjang([1, 2, 3])); // Output: Panjang: 3
console.log(tampilkan_panjang({ length: 10 })); // Output: Panjang: 10
// console.log(tampilkan_panjang(42)); // ❌ Error: number has no 'length'
// Constraint dengan interface
interface PunyaNama {
nama: string;
}
function sapa_semua<T extends PunyaNama>(items: T[]): void {
items.forEach(item => console.log(`Halo, ${item.nama}!`));
}
sapa_semua([{ nama: "Ali", umur: 25 }, { nama: "Budi", kota: "Jakarta" }]);
// Output:
// Halo, Ali!
// Halo, Budi!
Generic Interface dan Class
interface Repository<T> {
getById(id: number): T | undefined;
getAll(): T[];
save(item: T): void;
delete(id: number): boolean;
}
// Implementasi konkret
interface Produk {
id: number;
nama: string;
harga: number;
}
class ProdukRepository implements Repository<Produk> {
private data: Produk[] = [];
getById(id: number) {
return this.data.find(p => p.id === id);
}
getAll() { return [...this.data]; }
save(produk: Produk) {
const index = this.data.findIndex(p => p.id === produk.id);
if (index >= 0) {
this.data[index] = produk;
} else {
this.data.push(produk);
}
}
delete(id: number) {
const index = this.data.findIndex(p => p.id === id);
if (index < 0) return false;
this.data.splice(index, 1);
return true;
}
}
const repo = new ProdukRepository();
repo.save({ id: 1, nama: "Laptop", harga: 15_000_000 });
repo.save({ id: 2, nama: "Mouse", harga: 250_000 });
console.log(repo.getAll().length); // Output: 2
console.log(repo.getById(1)?.nama); // Output: Laptop
Class dengan Access Modifiers
class AkunBank {
private saldo: number; // Hanya bisa diakses dalam class
protected pemilik: string; // Bisa diakses subclass
public nomor_rekening: string; // Akses bebas
// Shorthand constructor — deklarasi dan inisialisasi sekaligus
constructor(
public readonly id: number, // readonly: tidak bisa diubah setelah init
protected nama_pemilik: string,
private saldo_awal: number
) {
this.saldo = saldo_awal;
this.pemilik = nama_pemilik;
this.nomor_rekening = `RK-${id.toString().padStart(6, "0")}`;
}
setor(jumlah: number): void {
if (jumlah <= 0) throw new Error("Jumlah harus positif");
this.saldo += jumlah;
}
get saldo_saat_ini(): number { // getter property
return this.saldo;
}
toString(): string {
return `${this.nomor_rekening} (${this.nama_pemilik}): Rp ${this.saldo.toLocaleString("id-ID")}`;
}
}
const akun = new AkunBank(1, "Ali Akbar", 1_000_000);
akun.setor(500_000);
console.log(akun.toString());
// Output: RK-000001 (Ali Akbar): Rp 1.500.000
console.log(akun.saldo_saat_ini);
// Output: 1500000
// akun.saldo = 0; // ❌ Error: private
// akun.id = 999; // ❌ Error: readonly
Utility Types Bawaan TypeScript
TypeScript menyediakan utility types untuk transformasi tipe yang umum:
interface Pengguna {
id: number;
nama: string;
email: string;
password: string;
umur: number;
}
// Partial<T> — semua properti menjadi optional
type UpdatePengguna = Partial<Pengguna>;
const update: UpdatePengguna = { nama: "Ali Baru" }; // Hanya update nama
// Required<T> — semua properti menjadi wajib (kebalikan Partial)
type PenggunаLengkap = Required<Pengguna>;
// Pick<T, Keys> — pilih beberapa properti saja
type ProfilPublik = Pick<Pengguna, "id" | "nama">;
const profil: ProfilPublik = { id: 1, nama: "Ali" }; // tanpa email, password, umur
// Omit<T, Keys> — hapus beberapa properti
type PenggunaResponse = Omit<Pengguna, "password">; // Jangan kirim password ke client!
const response: PenggunaResponse = { id: 1, nama: "Ali", email: "ali@email.com", umur: 25 };
// Readonly<T> — semua properti tidak bisa diubah
type KonfigReadonly = Readonly<{ host: string; port: number }>;
const config: KonfigReadonly = { host: "localhost", port: 3000 };
// config.port = 8080; // ❌ Error: readonly
// Record<Keys, Value> — membuat object type dari union keys
type StatusMap = Record<"aktif" | "nonaktif" | "suspend", string>;
const label: StatusMap = {
aktif: "Akun Aktif",
nonaktif: "Akun Tidak Aktif",
suspend: "Akun Ditangguhkan"
};
Pertanyaan yang Sering Diajukan
Apa perbedaan generics di TypeScript dengan di Java/C#?
Generics TypeScript hanya ada saat compile time — setelah dikompilasi ke JavaScript, informasi tipe dihapus (type erasure). Di Java, generics juga mengalami type erasure. Di C#, generics ada di runtime (reified generics) yang memberikan fleksibilitas lebih. Untuk penggunaan sehari-hari, perbedaan ini tidak terlalu terasa, tapi penting saat bekerja dengan reflection.
Kapan menggunakan private vs # (private field JavaScript)?
private TypeScript hanya dikenforce saat compile time — saat runtime (JavaScript), property masih bisa diakses. #nama adalah private field JavaScript yang dikenforce di runtime. Untuk kebanyakan kasus, private TypeScript sudah cukup. Gunakan # hanya jika benar-benar butuh privasi di runtime, misalnya untuk library yang dikonsumsi tanpa TypeScript.
Apa itu shorthand constructor (constructor(public nama: string))?
Ini adalah fitur TypeScript yang memungkinkan deklarasi parameter, assignment ke property, dan inisialisasi tipe — semuanya dalam satu baris. constructor(public nama: string) setara dengan: mendeklarasikan property nama: string, menerima parameter nama: string, dan mengeksekusi this.nama = nama. Sangat menghemat boilerplate untuk class dengan banyak property.
Apakah utility types bisa dikombinasikan?
Ya! Contoh: Partial<Omit<Pengguna, "password">> menghasilkan tipe di mana semua properti Pengguna (kecuali password) menjadi optional. Kombinasi ini sangat berguna untuk form update atau PATCH endpoint di API. Kamu juga bisa membuat utility type sendiri menggunakan type, keyof, dan mapped types.
Kesimpulan
| Konsep | Sintaks | Keterangan |
|---|---|---|
| Generic function | function f<T>(x: T): T | Satu fungsi, banyak tipe |
| Generic constraint | <T extends Interface> | T harus memenuhi syarat |
| Class private | private prop: type | Akses dalam class saja |
| Class readonly | readonly prop: type | Tidak bisa diubah setelah init |
| Shorthand ctor | constructor(public p: T) | Singkat deklarasi + assign |
| Partial<T> | Semua optional | Form update/PATCH |
| Pick<T, K> | Pilih beberapa field | Subset type |
| Omit<T, K> | Hapus beberapa field | Buang field sensitif |
Artikel sebelumnya: Interface dan Type Alias di TypeScript — mendefinisikan struktur objek.
Selamat! Kamu sudah menguasai fondasi TypeScript. Langkah selanjutnya: terapkan TypeScript dalam proyek nyata seperti React dengan TypeScript — cara membuat komponen React yang benar-benar type-safe.