Discriminated Union
Pola union paling rapi: bedakan varian dengan properti penanda (discriminant).
Kenapa ini pola union paling rapi
Masalah operator in adalah dia menebak dari properti yang kebetulan unik. Discriminated union membalik logikanya: kamu merancang union dengan properti penanda (discriminant) yang nilainya literal unik per varian. Compiler lalu bisa mempersempit hanya dari nilai satu properti itu. Dan yang lebih penting: dia bisa memaksa kamu menangani semua varian, tidak ada yang ke-skip diam-diam.
Alur narrowing langkah demi langkah
Skenario nyata: status pesanan di aplikasi kasir.
interface Menunggu { status: "menunggu"; dibuatPada: Date }
interface Diproses { status: "diproses"; kasir: string }
interface Selesai { status: "selesai"; totalBayar: number }
interface Batal { status: "batal"; alasan: string }
type Pesanan = Menunggu | Diproses | Selesai | Batal;
function labelStatus(p: Pesanan): string {
// Satu switch pada discriminant, tiap case otomatis menyempit.
switch (p.status) {
case "menunggu":
return `antri sejak ${p.dibuatPada.toLocaleTimeString()}`;
// p: Menunggu. p.kasir TIDAK bisa diakses di sini.
case "diproses":
return `dikerjakan ${p.kasir}`; // p: Diproses
case "selesai":
return `lunas Rp${p.totalBayar}`; // p: Selesai
case "batal":
return `batal: ${p.alasan}`; // p: Batal
}
}Di setiap case, compiler sudah tahu varian mana yang sedang ditangani, jadi properti spesifik varian itu bisa diakses langsung. Coba akses p.kasir di case "menunggu", langsung error: compiler tahu itu bukan Diproses.
Exhaustiveness check: jebakan varian baru
Suatu hari product minta status baru "dikirim". Tanpa pengaman, semua switch lama diam-diam tidak menanganinya, dan bug-nya baru ketahuan di production. Solusinya: paksa compiler protes dengan never.
function labelStatusAman(p: Pesanan): string {
switch (p.status) {
case "menunggu": return "antri";
case "diproses": return "dikerjakan";
case "selesai": return "lunas";
case "batal": return "batal";
default:
const takTerduga: never = p; // <-- pengaman
throw new Error(`status tak dikenal: ${takTerduga}`);
}
}Logikanya: kalau semua case sudah menangani semua varian, maka di default tidak ada yang tersisa, jadi p bertipe never. Begitu Pesanan ketambahan varian Dikirim, p di default bukan never lagi, dan compiler error:
error TS2322: Type 'Dikirim' is not assignable to type 'never'.Itu fitur, bukan bug: error ini menunjuk persis fungsi mana yang belum menangani varian baru. Di codebase besar dengan puluhan switch, ini penyelamat yang nyata.
Kapan dipakai di project nyata
- State async di frontend:
{ status: "loading" } | { status: "sukses"; data } | { status: "gagal"; error }adalah pola standar fetch data di React, Zustand, atau Redux. Komponen render beda UI per status tanpaisLoadingboolean yang gampang inkonsisten. - Hasil operasi di backend: service me-return
Berhasil | GagalValidasi | TidakKetemu, lalu handler HTTP memetakan tiap varian ke status code yang beda (200, 400, 404). Tidak ada lagi throw sembarangan. - Sistem event/pesan: bedakan
PesanTeks | PesanGambar | PesanLokasidi bot WhatsApp atau aplikasi chat dari fieldtipe, masing-masing dengan payload yang beda.
Catatan teknis: Discriminant tidak harus string literal, tapi string literal paling enak dibaca dan paling gampang di-switch. Hindari boolean sebagai discriminant (
isOk: true | false): cuma muat dua varian dan gampang kebalik artinya saat dibaca cepat.
Tantangan
State machine pembayaran
Buat discriminated union Bayar dengan varian: { tahap: "pilih" }, { tahap: "proses"; idTransaksi: string }, { tahap: "selesai"; struk: string }, { tahap: "batal"; alasan: string }. Fungsi pesan(b: Bayar): string memakai switch + exhaustiveness check never.
type Bayar =
| { tahap: "pilih" }
| { tahap: "proses"; idTransaksi: string }
| { tahap: "selesai"; struk: string }
| { tahap: "batal"; alasan: string };
function pesan(b: Bayar): string {
// switch + default never
}