Union Diskriminatif

Memodelkan data dengan field kind dan switch ekshaustif sebagai pola andalan.

Daftar isi

Union diskriminatif adalah gabungan beberapa tipe objek yang sama-sama memiliki satu properti berisi tipe literal, sehingga nilai literal itu menjadi penanda jenis anggota union yang sedang ditampung. Pola ini memecahkan kelemahan union biasa, yaitu compiler tidak mengetahui anggota mana yang sedang dipakai. Topik ini membahas penentuan properti pembeda, penanganan seluruh varian dengan switch, serta perilaku compiler ketika varian baru ditambahkan.

Properti pembeda dengan tipe literal#

Properti pembeda (discriminant) menautkan setiap anggota union dengan satu nilai literal yang unik, biasanya dinamai kind atau tag. Properti tersebut wajib bertipe literal, misalnya "baca", dan bukan string, karena pembeda string menghapus perbedaan antaranggota. Contoh berikut memodelkan pesan yang berdatangan dari perangkat sensor suhu.

ts pesan-perangkat.ts
type Pesan =
  | { kind: "baca"; suhu: number }
  | { kind: "kalibrasi"; titik: number }
  | { kind: "tidur"; durasi: number };

function proses(p: Pesan): string {
  switch (p.kind) {
    case "baca":
      return `baca suhu ${p.suhu} derajat`;
    case "kalibrasi":
      return `kalibrasi pada titik ${p.titik}`;
    case "tidur":
      return `tidur selama ${p.durasi} menit`;
    default:
      const sisa: never = p;
      return sisa;
  }
}

const antrean: Pesan[] = [
  { kind: "baca", suhu: 24 },
  { kind: "kalibrasi", titik: 3 },
  { kind: "tidur", durasi: 15 },
];

for (const p of antrean) {
  console.log(proses(p));
}
text Keluaran
baca suhu 24 derajat
kalibrasi pada titik 3
tidur selama 15 menit

Setiap varian pesan membawa data yang berbeda, yaitu suhu untuk pembacaan, titik untuk kalibrasi, dan durasi untuk tidur. Dengan pembeda kind, compiler mempersempit tipe p di dalam setiap case, sehingga akses p.suhu hanya ditulis pada cabang baca dan ditolak di cabang lain. Penulisan data satu varian menjadi terpusat pada satu tipe, dan fungsi proses tetap satu-satunya tempat penanganannya.

Model ini menggantikan dua kebiasaan yang rapuh, yaitu objek tunggal dengan banyak properti opsional dan penanda berupa angka yang maknanya harus dihafal. Properti opsional menutupi ketidakkonsistenan data karena semuanya boleh kosong, sedangkan pembeda literal membuat ketidaksesuaian terlihat oleh compiler. Daftar varian pada union sekaligus mendokumentasikan bentuk datanya.

Nama properti pembeda bebas dipilih, dan istilah kind, type, atau tag semuanya lazim dijumpai, asalkan konsisten pada seluruh varian. Yang menentukan adalah tipenya, yaitu tipe literal yang unik untuk setiap anggota union, sehingga satu nilai pembeda hanya menunjuk satu bentuk data. Angka juga sah sebagai pembeda, tetapi string literal lebih disarankan karena maknanya terbaca langsung pada kode maupun pada pesan error.

switch ekshaustif sebagai pola andalan#

Penanganan union diskriminatif yang paling andal berbentuk switch atas properti pembeda, dengan setiap anggota dibawa ke cabang tersendiri. Kata ekshaustif berarti seluruh varian tertangani, dan compiler menuntut hal itu melalui cabang default yang menugaskan nilai tersisa ke tipe never. Selama masih ada varian yang belum ditangani, nilai tersebut bukan never, sehingga penugasan ditolak dan error muncul di titik cabang yang terlewat.

Mekanisme ini menjadikan switch diskriminatif berbeda dari rangkaian if biasa. Rangkaian if tidak memberi peringatan apa pun ketika ada varian yang terlewat, sedangkan penugasan ke never mengubah kelalaian itu menjadi error kompilasi dengan lokasi yang jelas. Pola ini juga tetap bekerja ketika fungsi mengembalikan tipe lain, sebab yang diuji adalah tipe nilai tersisa, bukan bentuk kembaliannya.

Pola ini paling terasa manfaatnya pada fungsi yang wajib mengembalikan nilai. Setiap case yang diakhiri return menutup alurnya, dan cabang default dengan penugasan never menjamin tidak ada jalur yang berakhir tanpa kembalian. Compiler tidak lagi menuntut pernyataan kembalian tambahan di akhir fungsi, karena seluruh kemungkinan varian sudah tertangani.

Menambah varian baru#

Sifat ekshaustif baru benar-benar terlihat ketika model berkembang. Varian baru lazim muncul seiring bertambahnya jenis pesan yang dikirim perangkat, misalnya pemberitahuan bahwa baterai perlu diganti. Berkas berikut menambahkan varian ganti-baterai ke union tanpa menambah cabang case, sehingga perilaku compiler terhadap varian yang belum ditangani dapat dilihat.

ts pesan-salah.ts
// tidak-dijalankan
type Pesan =
  | { kind: "baca"; suhu: number }
  | { kind: "kalibrasi"; titik: number }
  | { kind: "tidur"; durasi: number }
  | { kind: "ganti-baterai"; persen: number };

function proses(p: Pesan): string {
  switch (p.kind) {
    case "baca":
      return `baca suhu ${p.suhu} derajat`;
    case "kalibrasi":
      return `kalibrasi pada titik ${p.titik}`;
    case "tidur":
      return `tidur selama ${p.durasi} menit`;
    default:
      const sisa: never = p;
      return sisa;
  }
}

console.log(proses({ kind: "ganti-baterai", persen: 20 }));
bash
# varian ganti-baterai belum punya cabang, sehingga penugasan ke never gagal
tsc --noEmit --pretty false pesan-salah.ts
text Keluaran
pesan-salah.ts(17,13): error TS2322: Type '{ kind: "ganti-baterai"; persen: number; }' is not assignable to type 'never'.

Pesan error menyebut secara lengkap bentuk varian yang tersisa, termasuk properti persen yang hanya dimiliki varian baru tersebut. Dari pesan itu terlihat cabang mana yang perlu ditulis dan data apa yang tersedia di dalamnya. Setelah cabang case "ganti-baterai" ditambahkan, nilai tersisa menjadi never lagi, dan pemeriksaan tipe lolos tanpa perubahan lain.

Proses menambah varian menjadi terarah dari awal hingga akhir. Union mendeklarasikan bentuk data barunya, sedangkan error compiler mendaftarkan titik yang harus disesuaikan. Daftar titik itu lengkap karena pemeriksaan mengikuti tipe, dan bukan hasil pencarian manual atas kata kunci di seluruh kode. Penangan lain yang memakai union yang sama juga menuntut cabang serupa pada tempatnya masing-masing, sehingga tidak ada titik yang tersembunyi.

Kesalahan umum#

Kekeliruan pertama adalah menuliskan properti pembeda bertipe string, karena pembeda yang bertipe umum tidak lagi membedakan anggota union. Compiler kehilangan kaitan antara nilai pembeda dan tipe anggota, sehingga akses properti khas varian ditolak di seluruh cabang.

ts pembeda-salah.ts
// tidak-dijalankan
type Baca = { kind: string; suhu: number };
type Kalibrasi = { kind: string; titik: number };

type Pesan = Baca | Kalibrasi;

function proses(p: Pesan): string {
  if (p.kind === "baca") {
    return `baca suhu ${p.suhu} derajat`;
  }
  return `kalibrasi pada titik ${p.titik}`;
}

console.log(proses({ kind: "baca", suhu: 24 }));
bash
# pembeda bertipe string tidak mempersempit union
tsc --noEmit --pretty false pembeda-salah.ts
text Keluaran
pembeda-salah.ts(9,27): error TS2339: Property 'suhu' does not exist on type 'Pesan'.
  Property 'suhu' does not exist on type 'Kalibrasi'.
pembeda-salah.ts(11,36): error TS2339: Property 'titik' does not exist on type 'Pesan'.
  Property 'titik' does not exist on type 'Baca'.

Perbaikannya adalah menuliskan pembeda sebagai tipe literal pada setiap varian, sehingga pembanding p.kind === "baca" kembali mempersempit tipe p. Kekeliruan kedua adalah memodelkan satu jenis data dengan objek berisi banyak properti opsional alih-alih union varian. Bentuk itu lolos pemeriksaan sekalipun kombinasi propertinya tidak masuk akal, misalnya pesan kalibrasi yang menyertakan suhu. Perbaikannya adalah memindahkan properti khas ke tipe variannya masing-masing dan menyisakan pembeda sebagai properti bersama.

Kekeliruan ketiga adalah menuliskan pembeda sebagai angka yang urutannya dihafal, misalnya 1 untuk baca dan 2 untuk kalibrasi. Compiler tetap menegakkan sifat ekshaustifnya, tetapi pembaca kode kehilangan maknanya, dan pesan error hanya menampilkan angka. String literal yang bermakna memberi penjagaan yang sama tanpa biaya tambahan.

Ringkasan#

  • Union diskriminatif menautkan setiap anggota union dengan satu properti literal, biasanya kind, yang menjadi penanda jenis angganya.
  • switch atas pembeda mempersempit tipe pada setiap cabang, sehingga akses properti khas varian aman dilakukan.
  • Penugasan ke never pada cabang default menjadikan percabangan ekshaustif, yaitu varian yang terlewat dilaporkan compiler lengkap dengan bentuknya.
  • Pembeda wajib bertipe literal, karena pembeda string menghilangkan kaitan antara nilai dan anggota union.

Referensi#