Kenapa ini penting
Di React modern kamu jarang menulis class komponen, tapi class tetap muncul di banyak tempat: class error kustom (class ApiError extends Error), SDK dan library (klien database, payment gateway), serta kode backend Node.js. TypeScript menambahkan fitur yang membuat class lebih aman: access modifier, implements, dan abstract.
Module (import/export) bahkan lebih penting: setiap file di proyekmu adalah module. TypeScript menambahkan import type dan export type, yang menjadi wajib ketika verbatimModuleSyntax aktif (default di tsc --init dan template Vite) serta ketika kode dijalankan dengan type stripping Node. Terakhir, operator satisfies membantu memeriksa object konfigurasi tanpa kehilangan tipe spesifiknya. Analogi untuk module: setiap file adalah ruangan dengan pintu. export menentukan barang apa yang boleh dibawa keluar, dan import type adalah membawa denah barang saja, bukan barangnya.
Konsep inti
Class dengan tipe
class Rekening {
pemilik: string;
private saldo: number;
readonly nomor: string;
constructor(pemilik: string, nomor: string, saldoAwal = 0) {
this.pemilik = pemilik;
this.nomor = nomor;
this.saldo = saldoAwal;
}
setor(jumlah: number): void {
if (jumlah <= 0) throw new Error("Jumlah harus positif");
this.saldo += jumlah;
}
lihatSaldo(): number {
return this.saldo;
}
}
Di mode strict (strictPropertyInitialization), setiap properti harus diisi di deklarasi atau di constructor.
Access modifier
| Modifier | Bisa diakses dari | Catatan |
|---|---|---|
public (default) |
Mana saja | Tidak perlu ditulis |
protected |
Class itu dan turunannya | |
private |
Hanya di dalam class itu | Diperiksa di compile time saja |
#nama (JavaScript) |
Hanya di dalam class itu | Benar-benar privat di runtime |
readonly |
Bisa dibaca, hanya diisi di constructor |
private milik TypeScript hilang saat kode dijalankan, sedangkan #saldo (private field JavaScript) benar-benar tidak bisa diakses dari luar. Untuk kode baru, # adalah pilihan yang lebih kuat dan juga lolos type stripping.
Parameter properties (dan kenapa sering dihindari)
TypeScript punya singkatan constructor(private saldo: number) {} yang otomatis membuat properti. Ringkas, tapi ini sintaks yang menghasilkan kode runtime, sehingga ditolak oleh type stripping Node dan oleh erasableSyntaxOnly (yang aktif di template Vite react-ts). Karena itu di materi ini kita menulis properti secara eksplisit.
implements
implements memeriksa bahwa class memenuhi bentuk sebuah interface. Ini kontrak: "class ini dijamin punya method-method berikut".
interface Penyimpanan {
simpan(kunci: string, nilai: string): void;
ambil(kunci: string): string | null;
}
class PenyimpananMemori implements Penyimpanan {
#data = new Map<string, string>();
simpan(kunci: string, nilai: string): void { this.#data.set(kunci, nilai); }
ambil(kunci: string): string | null { return this.#data.get(kunci) ?? null; }
}
Kamu bisa punya PenyimpananMemori untuk testing dan PenyimpananLokal (pakai localStorage) untuk production, dan kode lain cukup bergantung pada Penyimpanan.
abstract
Class abstract adalah "cetak biru setengah jadi": tidak bisa dibuat langsung dengan new, dan method abstract wajib diisi oleh turunannya.
abstract class Notifikasi {
abstract kirim(pesan: string): Promise<void>;
async kirimDenganLog(pesan: string): Promise<void> {
console.log(`Mengirim: ${pesan}`);
await this.kirim(pesan);
}
}
Module: import type dan export type
// tipe.ts
export type Produk = { id: number; nama: string };
export interface Keranjang { items: Produk[] }
// pemakai.ts
import type { Produk, Keranjang } from "./tipe.ts"; // seluruh import hanya tipe
import { hitungTotal, type Diskon } from "./harga.ts"; // campuran nilai + tipe
Kenapa penting? Tipe tidak ada di runtime. Dengan verbatimModuleSyntax, TypeScript menulis import persis seperti yang kamu tulis, sehingga kamu harus menandai mana yang hanya tipe agar bisa dibuang. Kalau lupa, tsc memberi error TS1484. Di Node dengan type stripping, import tipe tanpa type akan gagal saat runtime karena module tujuan tidak benar-benar mengekspor nilai itu.
Ingat juga aturan ekstensi: dengan moduleResolution: "bundler" (Vite, Next.js) kamu menulis import ... from "./tipe". Untuk Node dengan type stripping, tulis ekstensi lengkap "./tipe.ts" dan aktifkan allowImportingTsExtensions di tsconfig.
satisfies
satisfies memeriksa bahwa sebuah nilai memenuhi tipe tertentu tanpa mengubah tipe hasil inferensinya. Bandingkan:
type Rute = Record<string, { path: string; butuhLogin: boolean }>;
const rute1: Rute = { beranda: { path: "/", butuhLogin: false } };
rute1.berandaa; // tidak error! tipe rute1 adalah Record<string, ...> sehingga kunci apa pun dianggap ada
const rute2 = { beranda: { path: "/", butuhLogin: false } } satisfies Rute;
// rute2.berandaa → error, karena tipe rute2 tetap { beranda: {...} } yang spesifik
Pakai anotasi (: Tipe) ketika kamu ingin variabel bertipe umum. Pakai satisfies untuk object konfigurasi, peta warna, daftar rute, di mana kamu ingin dua-duanya: diperiksa sesuai aturan dan tetap tahu kunci-kuncinya secara spesifik.
Contoh kode
// class-module.ts
// 1. Error kustom: class yang paling sering kamu tulis di aplikasi web
class ApiError extends Error {
readonly status: number;
readonly url: string;
constructor(status: number, url: string, pesan: string) {
super(pesan);
this.name = "ApiError";
this.status = status;
this.url = url;
}
get bisaDiulang(): boolean {
return this.status >= 500;
}
}
function tanganiError(err: unknown): string {
if (err instanceof ApiError) {
return err.bisaDiulang ? `Server sibuk (${err.status}), coba lagi` : `Permintaan salah: ${err.message}`;
}
return "Kesalahan tidak dikenal";
}
console.log(tanganiError(new ApiError(503, "/api/produk", "Service Unavailable")));
// → Server sibuk (503), coba lagi
// 2. implements + private field
interface Penyimpanan {
simpan(kunci: string, nilai: string): void;
ambil(kunci: string): string | null;
}
class PenyimpananMemori implements Penyimpanan {
#data = new Map<string, string>();
simpan(kunci: string, nilai: string): void {
this.#data.set(kunci, nilai);
}
ambil(kunci: string): string | null {
return this.#data.get(kunci) ?? null;
}
}
class PenyimpananRusak implements Penyimpanan { // ❌ error TS2420: Class 'PenyimpananRusak' incorrectly implements interface 'Penyimpanan'.
simpan(kunci: string, nilai: string): void {
console.log(kunci, nilai);
}
}
const memori = new PenyimpananMemori();
memori.simpan("tema", "gelap");
console.log(memori.ambil("tema")); // → gelap
// 3. abstract class
abstract class Notifikasi {
abstract kirim(pesan: string): string;
kirimDenganWaktu(pesan: string): string {
return `[${new Date(2026, 9, 24, 9, 0).toLocaleTimeString("id-ID")}] ${this.kirim(pesan)}`;
}
}
class NotifikasiEmail extends Notifikasi {
#alamat: string;
constructor(alamat: string) {
super();
this.#alamat = alamat;
}
kirim(pesan: string): string {
return `Email ke ${this.#alamat}: ${pesan}`;
}
}
console.log(new NotifikasiEmail("sari@contoh.id").kirimDenganWaktu("Pesanan dikirim"));
// → [09.00.00] Email ke sari@contoh.id: Pesanan dikirim
const n = new Notifikasi(); // ❌ error TS2511: Cannot create an instance of an abstract class.
// 4. Access modifier
class Rekening {
private saldo = 0;
setor(jumlah: number): void {
this.saldo += jumlah;
}
}
const rek = new Rekening();
rek.setor(50000);
console.log(rek.saldo); // ❌ error TS2341: Property 'saldo' is private and only accessible within class 'Rekening'.
Contoh satisfies untuk konfigurasi tema:
// tema.ts
type Warna = `#${string}`; // template literal type: string yang diawali #
type PaletTema = Record<string, Warna>;
const palet = {
primer: "#2563eb",
sekunder: "#f59e0b",
bahaya: "#dc2626",
} satisfies PaletTema;
console.log(palet.primer); // → #2563eb (autocomplete tahu: primer, sekunder, bahaya)
console.log(palet.utama); // ❌ error TS2339: Property 'utama' does not exist on type '{ primer: "#2563eb"; sekunder: "#f59e0b"; bahaya: "#dc2626"; }'.
const paletSalah = {
primer: "2563eb", // ❌ error TS2322: Type '"2563eb"' is not assignable to type '`#${string}`'.
} satisfies PaletTema;
console.log(paletSalah);
Dan pola module yang benar dengan import type (dua file):
// src/tipe.ts
export type Todo = { id: number; judul: string; selesai: boolean };
export const BATAS_JUDUL = 80;
// src/todo.ts
import { BATAS_JUDUL } from "./tipe.ts"; // nilai: tetap ada di JavaScript hasil
import type { Todo } from "./tipe.ts"; // tipe: dibuang seluruhnya
export function buatTodo(id: number, judul: string): Todo {
return { id, judul: judul.slice(0, BATAS_JUDUL), selesai: false };
}
// Kalau ditulis `import { Todo } from "./tipe.ts"` dengan verbatimModuleSyntax aktif:
// error TS1484: 'Todo' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
Kesalahan umum
- Mengira
privatemelindungi data di runtime.privatehanya diperiksa compiler; di JavaScript properti itu tetap bisa diakses. Pakai#fielduntuk privasi sungguhan. - Memakai parameter properties atau
enumdi proyek denganerasableSyntaxOnly. Template Vitereact-tsdan type stripping Node menolaknya. Tulis properti eksplisit dan pakai union literal. - Lupa
import typeuntuk tipe. DenganverbatimModuleSyntaxhasilnya errorTS1484. Biasakanimport type { ... }atauimport { nilai, type Tipe }. - Memakai anotasi
: Tipeuntuk object konfigurasi lalu kehilangan kunci spesifik. Pakaisatisfiesagar tetap diperiksa sekaligus kuncinya dikenali. - Membuat class untuk segala hal. Di frontend, function dan object biasa sering lebih sederhana. Pakai class untuk error kustom, implementasi yang bisa diganti (
implements), atau ketika library memintanya.
Latihan
- Mudah, Buat class
Penghitungdengan private field#nilai, methodtambah(),reset(), dan getternilai. - Sedang, Buat
interface Logger { info(pesan: string): void; error(pesan: string): void }, lalu dua class:LoggerKonsol(menulis ke console) danLoggerDiam(tidak menulis apa pun, untuk testing). Tulis functionprosesPesanan(logger: Logger). - Menantang, Buat object
MENU_NAVIGASIyangsatisfies Record<string, { label: string; path: \/${string}` }>, lalu turunkantype KunciMenu = keyof typeof MENU_NAVIGASIdan tulis functionlinkKe(kunci: KunciMenu): string`.
Lihat solusi
// 1. Mudah
class Penghitung {
#nilai = 0;
tambah(): void { this.#nilai++; }
reset(): void { this.#nilai = 0; }
get nilai(): number { return this.#nilai; }
}
const p = new Penghitung();
p.tambah(); p.tambah();
console.log(p.nilai); // → 2
// 2. Sedang
interface Logger {
info(pesan: string): void;
error(pesan: string): void;
}
class LoggerKonsol implements Logger {
info(pesan: string): void { console.log(`INFO: ${pesan}`); }
error(pesan: string): void { console.error(`ERROR: ${pesan}`); }
}
class LoggerDiam implements Logger {
info(): void {}
error(): void {}
}
function prosesPesanan(logger: Logger): void {
logger.info("Pesanan #12 diproses");
}
prosesPesanan(new LoggerKonsol()); // → INFO: Pesanan #12 diproses
prosesPesanan(new LoggerDiam()); // (tidak ada output)
// 3. Menantang
const MENU_NAVIGASI = {
beranda: { label: "Beranda", path: "/" },
produk: { label: "Produk", path: "/produk" },
kontak: { label: "Kontak", path: "/kontak" },
} satisfies Record<string, { label: string; path: `/${string}` }>;
type KunciMenu = keyof typeof MENU_NAVIGASI; // "beranda" | "produk" | "kontak"
function linkKe(kunci: KunciMenu): string {
const m = MENU_NAVIGASI[kunci];
return `<a href="${m.path}">${m.label}</a>`;
}
console.log(linkKe("produk")); // → <a href="/produk">Produk</a>
LoggerDiam boleh menulis method tanpa parameter karena function dengan parameter lebih sedikit tetap kompatibel dengan tipe yang meminta lebih banyak parameter.
Ringkasan
- Class TypeScript punya
public,protected,private, danreadonly; untuk privasi runtime pakai#field. implementsmemastikan class memenuhi interface;abstractmembuat cetak biru yang wajib dilengkapi turunannya.- Hindari parameter properties dan
enumdi proyek denganerasableSyntaxOnlyatau type stripping Node. - Pakai
import type/export typeuntuk tipe; denganverbatimModuleSyntaxini wajib. satisfiesmemeriksa nilai terhadap tipe tanpa menghilangkan tipe spesifik hasil inferensi.