Kelas #

Kelas adalah mekanisme utama pemrograman berorientasi objek (OOP) — ia menggabungkan data (properti) dan perilaku (metode) ke dalam satu unit yang kohesif. TypeScript membawa kelas JavaScript ke level baru dengan menambahkan access modifier, properti readonly, abstract method, dan integrasi yang mulus dengan sistem tipe melalui interface. Namun, kelas bukan satu-satunya cara mengorganisir kode di TypeScript — dan tidak selalu pilihan yang tepat. Memahami kelas di TypeScript berarti memahami kapan ia berguna, bagaimana merancangnya dengan prinsip yang baik, dan kapan lebih baik menggunakan pendekatan lain seperti fungsi atau plain object.

Anatomi Kelas TypeScript #

Sebuah kelas TypeScript bisa memiliki banyak komponen. Berikut gambaran lengkap dari satu kelas yang representatif:

class Rekening {
  // 1. Properti dengan access modifier
  private saldo: number;
  protected pemilik: string;
  public readonly nomorRekening: string;

  // 2. Properti statis — milik kelas, bukan instance
  private static jumlahRekening = 0;

  // 3. Constructor — dipanggil saat new Rekening(...)
  constructor(pemilik: string, saldoAwal: number) {
    this.pemilik = pemilik;
    this.saldo = saldoAwal;
    this.nomorRekening = Rekening.buatNomor();
    Rekening.jumlahRekening++;
  }

  // 4. Metode instance
  setor(jumlah: number): void {
    if (jumlah <= 0) throw new Error("Jumlah setor harus positif");
    this.saldo += jumlah;
  }

  // 5. Getter — akses seperti properti, tapi bisa ada logika
  get saldoSaatIni(): number {
    return this.saldo;
  }

  // 6. Metode statis — dipanggil lewat nama kelas
  static get totalRekening(): number {
    return Rekening.jumlahRekening;
  }

  private static buatNomor(): string {
    return `RK-${Date.now()}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;
  }
}

const rek = new Rekening("Budi", 1_000_000);
rek.setor(500_000);
console.log(rek.saldoSaatIni);         // 1500000
console.log(rek.nomorRekening);        // "RK-1746..."
console.log(Rekening.totalRekening);   // 1

Shorthand Constructor Parameter #

TypeScript menyediakan sintaks ringkas yang menggabungkan deklarasi properti dan inisialisasi dalam satu baris di constructor — sangat mengurangi boilerplate:

// ANTI-PATTERN: Verbose — deklarasi dan inisialisasi terpisah
class PenggunaPanjang {
  nama: string;
  email: string;
  private usia: number;

  constructor(nama: string, email: string, usia: number) {
    this.nama = nama;
    this.email = email;
    this.usia = usia;
  }
}

// BENAR: Shorthand — deklarasikan langsung di parameter constructor
class Pengguna {
  constructor(
    public nama: string,
    public email: string,
    private usia: number,
    protected peran: string = "pengguna",
    public readonly id: string = crypto.randomUUID()
  ) {}

  info(): string {
    return `${this.nama} <${this.email}> [${this.peran}]`;
  }
}

const pengguna = new Pengguna("Budi", "[email protected]", 25);
console.log(pengguna.info()); // "Budi <[email protected]> [pengguna]"
// pengguna.id = "lain"; // ✗ Error: Cannot assign to 'id' because it is a read-only property

Access Modifier: public, private, protected #

Access modifier mengontrol di mana sebuah properti atau metode bisa diakses. TypeScript menegakkan aturan ini saat kompilasi:

class KonfigurasiDatabase {
  public host: string;           // Bisa diakses dari mana saja
  protected port: number;        // Hanya kelas ini dan turunannya
  private password: string;      // Hanya di dalam kelas ini
  readonly namaDatabase: string; // Bisa dibaca, tidak bisa diubah

  constructor(host: string, port: number, password: string, namaDb: string) {
    this.host = host;
    this.port = port;
    this.password = password;
    this.namaDatabase = namaDb;
  }

  buatKoneksiString(): string {
    // Bisa akses semua — kita ada di dalam kelas
    return `postgresql://${this.host}:${this.port}/${this.namaDatabase}`;
    // Password sengaja tidak dimasukkan ke connection string untuk keamanan
  }
}

const dbKonfig = new KonfigurasiDatabase("localhost", 5432, "r4h4s1a", "muslimapps");
console.log(dbKonfig.host);          // ✓ public
console.log(dbKonfig.namaDatabase);  // ✓ readonly
// dbKonfig.port;                    // ✗ Error: 'port' is protected
// dbKonfig.password;                // ✗ Error: 'password' is private
// dbKonfig.namaDatabase = "lain";   // ✗ Error: read-only

Private Field JavaScript (#) vs private TypeScript #

TypeScript 4.3+ mendukung private field JavaScript native menggunakan #. Perbedaan krusialnya: private TypeScript hanya ada saat kompilasi, sedangkan # bersifat private di level runtime JavaScript:

class KontainerRahasia {
  private tipeTS: string;  // Private TypeScript — hanya saat kompilasi
  #tipeJS: string;          // Private JavaScript — private sungguhan di runtime

  constructor(nilai: string) {
    this.tipeTS = nilai;
    this.#tipeJS = nilai;
  }
}

const k = new KontainerRahasia("rahasia");

// Di JavaScript compiled output:
// (k as any).tipeTS; // ✓ Bisa diakses! TypeScript hanya melindungi saat kompilasi
// (k as any)["#tipeJS"]; // ✗ Tidak bisa — private field benar-benar tersembunyi

Getter dan Setter #

Getter dan setter adalah metode khusus yang dipanggil seperti properti biasa. Getter untuk membaca nilai (bisa dengan logika), setter untuk menulis nilai dengan validasi:

class TemperatureSensor {
  private _celsius: number;

  constructor(celsius: number) {
    this._celsius = celsius;
  }

  // Getter — baca properti dengan logika
  get celsius(): number {
    return this._celsius;
  }

  get fahrenheit(): number {
    return (this._celsius * 9) / 5 + 32;
  }

  get kelvin(): number {
    return this._celsius + 273.15;
  }

  // Setter — tulis dengan validasi
  set celsius(nilai: number) {
    if (nilai < -273.15) {
      throw new RangeError("Temperatur tidak bisa di bawah nol absolut (-273.15°C)");
    }
    this._celsius = nilai;
  }
}

const sensor = new TemperatureSensor(100);
console.log(sensor.celsius);     // 100
console.log(sensor.fahrenheit);  // 212
console.log(sensor.kelvin);      // 373.15

sensor.celsius = 37; // Memanggil setter
// sensor.celsius = -300; // ✗ Melempar RangeError saat runtime

Pewarisan dengan extends dan super #

Pewarisan memungkinkan kelas anak mewarisi semua properti dan metode dari kelas induk, lalu menambahkan atau mengubah perilakunya:

class Hewan {
  constructor(
    protected nama: string,
    protected jenisKelamin: "jantan" | "betina"
  ) {}

  bergerak(jarakMeter: number): void {
    console.log(`${this.nama} bergerak ${jarakMeter} meter`);
  }

  bersuara(): void {
    console.log(`${this.nama} mengeluarkan suara`);
  }

  deskripsi(): string {
    return `${this.nama} (${this.jenisKelamin})`;
  }
}

class Kucing extends Hewan {
  private warnaBulu: string;

  constructor(nama: string, jenisKelamin: "jantan" | "betina", warnaBulu: string) {
    super(nama, jenisKelamin); // Wajib panggil super() sebelum akses this
    this.warnaBulu = warnaBulu;
  }

  // Override metode induk
  bersuara(): void {
    console.log(`${this.nama}: Meow!`);
  }

  // Tambah metode baru
  mendengkur(): void {
    console.log(`${this.nama}: Purrr...`);
  }

  // Perluas metode induk
  override deskripsi(): string {
    return `${super.deskripsi()}, bulu ${this.warnaBulu}`;
  }
}

const kucing = new Kucing("Mochi", "betina", "oranye");
kucing.bergerak(5);           // "Mochi bergerak 5 meter"
kucing.bersuara();            // "Mochi: Meow!" — override
kucing.mendengkur();          // "Mochi: Purrr..."
console.log(kucing.deskripsi()); // "Mochi (betina), bulu oranye"

// instanceof bekerja sepanjang rantai warisan
console.log(kucing instanceof Kucing); // true
console.log(kucing instanceof Hewan);  // true

Kata Kunci override (TypeScript 4.3+) #

Menambahkan override secara eksplisit pada metode yang mengoverride metode induk membuat TypeScript memvalidasi bahwa metode tersebut benar-benar ada di kelas induk:

class KucingAman extends Hewan {
  // ✓ TypeScript memverifikasi bahwa Hewan punya metode 'bersuara'
  override bersuara(): void {
    console.log("Meow!");
  }

  // ✗ Error: "tidur" tidak ada di Hewan — typo terdeteksi!
  // override tidur(): void { ... }
}

Static Member — Milik Kelas, Bukan Instance #

Properti dan metode static tidak terikat pada instance manapun — mereka hidup di kelas itu sendiri. Berguna untuk factory method, utilitas, dan singleton:

class IDGenerator {
  private static counter = 0;
  private static readonly PREFIX = "ID";

  // Factory method — cara idiomatik membuat instance dengan logika
  static buat(): IDGenerator {
    return new IDGenerator();
  }

  static berikutnya(): string {
    IDGenerator.counter++;
    return `${IDGenerator.PREFIX}-${String(IDGenerator.counter).padStart(6, "0")}`;
  }

  static reset(): void {
    IDGenerator.counter = 0;
  }
}

console.log(IDGenerator.berikutnya()); // "ID-000001"
console.log(IDGenerator.berikutnya()); // "ID-000002"
console.log(IDGenerator.berikutnya()); // "ID-000003"

// Singleton pattern menggunakan static
class KonfigurasiApp {
  private static instance: KonfigurasiApp | null = null;
  private readonly pengaturan: Map<string, string>;

  private constructor() {
    this.pengaturan = new Map([
      ["tema", "gelap"],
      ["bahasa", "id"],
    ]);
  }

  static getInstance(): KonfigurasiApp {
    if (!KonfigurasiApp.instance) {
      KonfigurasiApp.instance = new KonfigurasiApp();
    }
    return KonfigurasiApp.instance;
  }

  ambil(kunci: string): string | undefined {
    return this.pengaturan.get(kunci);
  }
}

const konfig1 = KonfigurasiApp.getInstance();
const konfig2 = KonfigurasiApp.getInstance();
console.log(konfig1 === konfig2); // true — instance yang sama

Abstract Class — Kontrak Implementasi #

Kelas abstrak tidak bisa diinstansiasi langsung — ia adalah template yang harus diimplementasikan oleh kelas turunan. Perbedaan kunci dengan interface: abstract class bisa memiliki implementasi konkret sekaligus metode abstrak:

abstract class PenyimpananData {
  // Metode abstrak — WAJIB diimplementasikan oleh kelas turunan
  abstract simpan(kunci: string, nilai: string): Promise<void>;
  abstract ambil(kunci: string): Promise<string | null>;
  abstract hapus(kunci: string): Promise<void>;

  // Metode konkret — sudah ada implementasinya, bisa dioverride atau tidak
  async simpanJSON<T>(kunci: string, nilai: T): Promise<void> {
    await this.simpan(kunci, JSON.stringify(nilai));
  }

  async ambilJSON<T>(kunci: string): Promise<T | null> {
    const raw = await this.ambil(kunci);
    if (raw === null) return null;
    return JSON.parse(raw) as T;
  }
}

// Implementasi konkret untuk Redis
class PenyimpananRedis extends PenyimpananData {
  constructor(private readonly url: string) {
    super();
  }

  async simpan(kunci: string, nilai: string): Promise<void> {
    console.log(`[Redis] SET ${kunci} = ${nilai}`);
    // Implementasi aktual Redis di sini
  }

  async ambil(kunci: string): Promise<string | null> {
    console.log(`[Redis] GET ${kunci}`);
    return null; // Simulasi: tidak ada data
  }

  async hapus(kunci: string): Promise<void> {
    console.log(`[Redis] DEL ${kunci}`);
  }
}

// Implementasi konkret untuk memori (untuk testing)
class PenyimpananMemori extends PenyimpananData {
  private store = new Map<string, string>();

  async simpan(kunci: string, nilai: string): Promise<void> {
    this.store.set(kunci, nilai);
  }

  async ambil(kunci: string): Promise<string | null> {
    return this.store.get(kunci) ?? null;
  }

  async hapus(kunci: string): Promise<void> {
    this.store.delete(kunci);
  }
}

// new PenyimpananData(); // ✗ Error: Cannot create an instance of an abstract class
const storage = new PenyimpananMemori();
await storage.simpanJSON("pengguna", { nama: "Budi", usia: 25 });
const data = await storage.ambilJSON("pengguna");

Mengimplementasikan Interface #

Kelas bisa mengimplementasikan satu atau lebih interface dengan kata kunci implements. Ini memastikan kelas memenuhi kontrak tipe yang ditetapkan interface:

interface BisaDiedit {
  edit(data: Partial<this>): void;
  validasi(): boolean;
}

interface BisaDihapus {
  hapus(): void;
  pulihkan(): void;
}

interface Entitas {
  readonly id: string;
  dibuatPada: Date;
  diperbarui: Date;
}

// Kelas bisa mengimplementasikan beberapa interface sekaligus
class Artikel implements Entitas, BisaDiedit, BisaDihapus {
  readonly id: string;
  dibuatPada: Date;
  diperbarui: Date;
  private terhapus = false;

  constructor(
    public judul: string,
    public konten: string
  ) {
    this.id = crypto.randomUUID();
    this.dibuatPada = new Date();
    this.diperbarui = new Date();
  }

  edit(data: Partial<Artikel>): void {
    if (data.judul !== undefined) this.judul = data.judul;
    if (data.konten !== undefined) this.konten = data.konten;
    this.diperbarui = new Date();
  }

  validasi(): boolean {
    return this.judul.length >= 5 && this.konten.length >= 50;
  }

  hapus(): void {
    this.terhapus = true;
    console.log(`Artikel "${this.judul}" ditandai terhapus`);
  }

  pulihkan(): void {
    this.terhapus = false;
    console.log(`Artikel "${this.judul}" dipulihkan`);
  }
}

Mixin — Komposisi Horizontal #

TypeScript tidak mendukung multiple inheritance (mewarisi dari lebih dari satu kelas), tapi mendukung mixin — cara mengkomposisi perilaku dari beberapa sumber tanpa hirarki warisan:

// Tipe untuk constructor
type Constructor<T = {}> = new (...args: any[]) => T;

// Mixin 1: Menambahkan kemampuan logging
function DenganLogging<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    log(pesan: string): void {
      console.log(`[${new Date().toISOString()}] ${pesan}`);
    }
  };
}

// Mixin 2: Menambahkan kemampuan serialisasi
function DenganSerialisasi<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    keJSON(): string {
      return JSON.stringify(this);
    }

    static dariJSON<T>(json: string): T {
      return JSON.parse(json) as T;
    }
  };
}

// Kelas dasar
class KomponenDasar {
  constructor(public nama: string) {}
}

// Komposisi mixin — gabungkan beberapa kemampuan
const KomponenLengkap = DenganLogging(DenganSerialisasi(KomponenDasar));

const komponen = new KomponenLengkap("KomponenSaya");
komponen.log("Komponen diinisialisasi");  // Dari DenganLogging
console.log(komponen.keJSON());            // Dari DenganSerialisasi

Hierarki Kelas — Visualisasi #

classDiagram
    class PenyimpananData {
        <<abstract>>
        +simpanJSON(kunci, nilai) Promise
        +ambilJSON(kunci) Promise
        #simpan(kunci, nilai)* Promise
        #ambil(kunci)* Promise
        #hapus(kunci)* Promise
    }

    class PenyimpananRedis {
        -url: string
        +simpan(kunci, nilai) Promise
        +ambil(kunci) Promise
        +hapus(kunci) Promise
    }

    class PenyimpananMemori {
        -store: Map
        +simpan(kunci, nilai) Promise
        +ambil(kunci) Promise
        +hapus(kunci) Promise
    }

    class Entitas {
        <<interface>>
        +id: string
        +dibuatPada: Date
        +diperbarui: Date
    }

    class BisaDiedit {
        <<interface>>
        +edit(data) void
        +validasi() boolean
    }

    class Artikel {
        +judul: string
        +konten: string
        -terhapus: boolean
        +edit(data) void
        +validasi() boolean
        +hapus() void
        +pulihkan() void
    }

    PenyimpananData <|-- PenyimpananRedis : extends
    PenyimpananData <|-- PenyimpananMemori : extends
    Entitas <|.. Artikel : implements
    BisaDiedit <|.. Artikel : implements

Ringkasan #

  • Shorthand constructor parameter — deklarasikan properti langsung di parameter constructor dengan access modifier (public, private, protected, readonly) untuk mengurangi boilerplate secara signifikan.
  • private TypeScript vs # JavaScriptprivate hanya ada saat kompilasi dan bisa ditembus dengan as any; # adalah private field JavaScript yang benar-benar tersembunyi di runtime, gunakan # jika butuh enkapsulasi yang sesungguhnya.
  • Getter dan setter memungkinkan akses properti yang terlihat seperti properti biasa tapi bisa memiliki logika validasi atau transformasi; sangat berguna untuk properti turunan dan properti dengan validasi input.
  • override keyword (TypeScript 4.3+) membuat TypeScript memvalidasi bahwa metode yang di-override benar-benar ada di kelas induk — mencegah bug dari typo nama metode.
  • Abstract class cocok saat ada implementasi default yang ingin dibagi ke semua kelas turunan, sekaligus ada beberapa metode yang harus diimplementasikan masing-masing; berbeda dari interface yang murni kontrak tanpa implementasi.
  • Static member untuk factory method, singleton, dan utilitas yang tidak membutuhkan state instance — akses lewat nama kelas, bukan instance.
  • Implements beberapa interface memungkinkan kelas memenuhi beberapa kontrak sekaligus tanpa terikat hierarki warisan yang rigid.
  • Mixin untuk komposisi horizontal — TypeScript tidak mendukung multiple inheritance, tapi mixin memungkinkan mengkomposisi perilaku dari beberapa sumber; lebih fleksibel dari hierarki warisan yang dalam.
  • Favoritkan komposisi daripada warisan — warisan yang dalam (A extends B extends C extends D) membuat kode sulit diubah dan diuji; lebih baik gunakan komposisi (mixin, dependency injection) untuk berbagi perilaku.

← Sebelumnya: Fungsi   Berikutnya: Interface →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact