Bahasa Indonesia

Jelajahi discriminated unions di TypeScript, sebuah alat yang kuat untuk membangun state machine yang kokoh dan type-safe. Pelajari cara mendefinisikan state, menangani transisi, dan memanfaatkan sistem tipe TypeScript untuk meningkatkan keandalan kode.

TypeScript Discriminated Unions: Membangun State Machine yang Type-Safe

Dalam dunia pengembangan perangkat lunak, mengelola state aplikasi secara efektif sangatlah penting. State machine menyediakan abstraksi yang kuat untuk memodelkan sistem stateful yang kompleks, memastikan perilaku yang dapat diprediksi dan menyederhanakan pemahaman tentang logika sistem. TypeScript, dengan sistem tipenya yang kokoh, menawarkan mekanisme fantastis untuk membangun state machine yang type-safe menggunakan discriminated unions (juga dikenal sebagai tagged unions atau algebraic data types).

Apa itu Discriminated Unions?

Discriminated union adalah sebuah tipe yang merepresentasikan sebuah nilai yang bisa menjadi salah satu dari beberapa tipe yang berbeda. Setiap tipe ini, yang dikenal sebagai anggota dari union, memiliki properti umum yang khas yang disebut diskriminan atau tag. Diskriminan ini memungkinkan TypeScript untuk menentukan dengan tepat anggota mana dari union yang sedang aktif, memungkinkan pengecekan tipe dan pelengkapan otomatis (auto-completion) yang kuat.

Bayangkan seperti lampu lalu lintas. Lampu tersebut bisa berada dalam salah satu dari tiga keadaan: Merah, Kuning, atau Hijau. Properti 'warna' bertindak sebagai diskriminan, yang memberitahu kita secara pasti keadaan lampu saat ini.

Mengapa Menggunakan Discriminated Unions untuk State Machine?

Discriminated unions memberikan beberapa manfaat utama saat membangun state machine di TypeScript:

Mendefinisikan State Machine dengan Discriminated Unions

Mari kita ilustrasikan cara mendefinisikan state machine menggunakan discriminated unions dengan contoh praktis: sistem pemrosesan pesanan. Sebuah pesanan dapat berada dalam state berikut: Pending, Processing, Shipped, dan Delivered.

Langkah 1: Definisikan Tipe State

Pertama, kita mendefinisikan tipe individual untuk setiap state. Setiap tipe akan memiliki properti `type` yang bertindak sebagai diskriminan, beserta data spesifik untuk state tersebut.


interface Pending {
  type: "pending";
  orderId: string;
  customerName: string;
  items: string[];
}

interface Processing {
  type: "processing";
  orderId: string;
  assignedAgent: string;
}

interface Shipped {
  type: "shipped";
  orderId: string;
  trackingNumber: string;
}

interface Delivered {
  type: "delivered";
  orderId: string;
  deliveryDate: Date;
}

Langkah 2: Buat Tipe Discriminated Union

Selanjutnya, kita membuat discriminated union dengan menggabungkan tipe-tipe individual ini menggunakan operator `|` (union).


type OrderState = Pending | Processing | Shipped | Delivered;

Sekarang, `OrderState` merepresentasikan sebuah nilai yang bisa berupa `Pending`, `Processing`, `Shipped`, atau `Delivered`. Properti `type` di dalam setiap state bertindak sebagai diskriminan, memungkinkan TypeScript untuk membedakannya.

Menangani Transisi State

Setelah kita mendefinisikan state machine kita, kita memerlukan mekanisme untuk beralih antar state. Mari kita buat fungsi `processOrder` yang menerima state saat ini dan sebuah action sebagai input, lalu mengembalikan state yang baru.


interface Action {
  type: string;
  payload?: any;
}

function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    case "pending":
      if (action.type === "startProcessing") {
        return {
          type: "processing",
          orderId: state.orderId,
          assignedAgent: action.payload.agentId,
        };
      }
      return state; // Tidak ada perubahan state

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // Tidak ada perubahan state

    case "shipped":
      if (action.type === "deliverOrder") {
        return {
          type: "delivered",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // Tidak ada perubahan state

    case "delivered":
      // Pesanan sudah terkirim, tidak ada tindakan lebih lanjut
      return state;

    default:
      // Ini seharusnya tidak pernah terjadi karena pengecekan kelengkapan
      return state; // Atau lemparkan galat
  }
}

Penjelasan

Memanfaatkan Pengecekan Kelengkapan (Exhaustiveness Checking)

Pengecekan kelengkapan dari TypeScript adalah fitur yang kuat yang memastikan Anda menangani semua state yang mungkin dalam state machine Anda. Jika Anda menambahkan state baru ke union `OrderState` tetapi lupa memperbarui fungsi `processOrder`, TypeScript akan menandai sebuah galat.

Untuk mengaktifkan pengecekan kelengkapan, Anda dapat menggunakan tipe `never`. Di dalam kasus `default` dari pernyataan switch Anda, tetapkan state ke variabel dengan tipe `never`.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (kasus sebelumnya) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // Atau lemparkan galat
  }
}

Jika pernyataan `switch` menangani semua nilai `OrderState` yang mungkin, variabel `_exhaustiveCheck` akan bertipe `never` dan kode akan berhasil dikompilasi. Namun, jika Anda menambahkan state baru ke union `OrderState` dan lupa menanganinya di pernyataan `switch`, variabel `_exhaustiveCheck` akan memiliki tipe yang berbeda, dan TypeScript akan melemparkan galat waktu kompilasi (compile-time error), memberitahu Anda tentang kasus yang hilang.

Contoh Praktis dan Aplikasi

Discriminated unions dapat diterapkan dalam berbagai skenario selain sistem pemrosesan pesanan sederhana:

Contoh: Manajemen State UI

Mari kita pertimbangkan contoh sederhana dalam mengelola state komponen UI yang mengambil data dari API. Kita dapat mendefinisikan state berikut:


interface Initial {
  type: "initial";
}

interface Loading {
  type: "loading";
}

interface Success {
  type: "success";
  data: T;
}

interface Error {
  type: "error";
  message: string;
}

type UIState = Initial | Loading | Success | Error;

function renderUI(state: UIState): React.ReactNode {
  switch (state.type) {
    case "initial":
      return 

Klik tombol untuk memuat data.

; case "loading": return

Memuat...

; case "success": return
{JSON.stringify(state.data, null, 2)}
; case "error": return

Galat: {state.message}

; default: const _exhaustiveCheck: never = state; return _exhaustiveCheck; } }

Contoh ini menunjukkan bagaimana discriminated unions dapat digunakan untuk mengelola berbagai state dari komponen UI secara efektif, memastikan UI dirender dengan benar berdasarkan state saat ini. Fungsi `renderUI` menangani setiap state dengan tepat, menyediakan cara yang jelas dan type-safe untuk mengelola UI.

Praktik Terbaik Menggunakan Discriminated Unions

Untuk memanfaatkan discriminated unions secara efektif dalam proyek TypeScript Anda, pertimbangkan praktik terbaik berikut:

Teknik Tingkat Lanjut

Conditional Types

Conditional types dapat digabungkan dengan discriminated unions untuk menciptakan state machine yang lebih kuat dan fleksibel. Sebagai contoh, Anda dapat menggunakan conditional types untuk mendefinisikan tipe kembalian yang berbeda untuk sebuah fungsi berdasarkan state saat ini.


function getData(state: UIState): T | undefined {
  if (state.type === "success") {
    return state.data;
  }
  return undefined;
}

Fungsi ini menggunakan pernyataan `if` sederhana tetapi bisa dibuat lebih kokoh menggunakan conditional types untuk memastikan tipe tertentu selalu dikembalikan.

Utility Types

Utility types dari TypeScript, seperti `Extract` dan `Omit`, dapat sangat membantu saat bekerja dengan discriminated unions. `Extract` memungkinkan Anda untuk mengekstrak anggota spesifik dari sebuah tipe union berdasarkan kondisi, sementara `Omit` memungkinkan Anda untuk menghapus properti dari sebuah tipe.


// Ekstrak state "success" dari union UIState
type SuccessState = Extract, { type: "success" }>;

// Hilangkan properti 'message' dari interface Error
type ErrorWithoutMessage = Omit;

Contoh Dunia Nyata di Berbagai Industri

Kekuatan discriminated unions meluas ke berbagai industri dan domain aplikasi:

Kesimpulan

Discriminated unions di TypeScript menyediakan cara yang kuat dan type-safe untuk membangun state machine. Dengan mendefinisikan state dan transisi yang mungkin secara jelas, Anda dapat membuat kode yang lebih kokoh, mudah dipelihara, dan dapat dimengerti. Kombinasi dari keamanan tipe, pengecekan kelengkapan, dan pelengkapan kode yang ditingkatkan membuat discriminated unions menjadi alat yang tak ternilai bagi setiap pengembang TypeScript yang berurusan dengan manajemen state yang kompleks. Terapkan discriminated unions di proyek Anda berikutnya dan rasakan manfaat manajemen state yang type-safe secara langsung. Seperti yang telah kami tunjukkan dengan berbagai contoh dari e-commerce hingga kesehatan, dan logistik hingga pendidikan, prinsip manajemen state yang type-safe melalui discriminated unions dapat diterapkan secara universal.

Baik Anda membangun komponen UI sederhana atau aplikasi perusahaan yang kompleks, discriminated unions dapat membantu Anda mengelola state secara lebih efektif dan mengurangi risiko galat saat runtime. Jadi, selami dan jelajahi dunia state machine yang type-safe dengan TypeScript!