Langsung ke konten
KamusNgoding
Menengah Typescript 3 menit baca

Generics dan Class di TypeScript

#typescript #generics #class #access-modifiers #implements #abstract #utility-types
📚

Baca dulu sebelum ini:

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

KonsepSintaksKeterangan
Generic functionfunction f<T>(x: T): TSatu fungsi, banyak tipe
Generic constraint<T extends Interface>T harus memenuhi syarat
Class privateprivate prop: typeAkses dalam class saja
Class readonlyreadonly prop: typeTidak bisa diubah setelah init
Shorthand ctorconstructor(public p: T)Singkat deklarasi + assign
Partial<T>Semua optionalForm update/PATCH
Pick<T, K>Pilih beberapa fieldSubset type
Omit<T, K>Hapus beberapa fieldBuang 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.

Artikel Terkait