JAVASCRIPT TYPESCRIPT FUNCTIONAL-PROGRAMMING ERROR-HANDLING CLEAN-CODE SOFTWARE-DESIGN PREDICTABILITY MONADS SOFTWARE-ENGINEERING ADVANCED-JAVASCRIPT DEVELOPER-EXPERIENCE

Monads di JavaScript/TypeScript: Mengelola Error dengan Gaya Fungsional yang Elegan dan Prediktif

⏱️ 9 menit baca
👨‍💻

Monads di JavaScript/TypeScript: Mengelola Error dengan Gaya Fungsional yang Elegan dan Prediktif

1. Pendahuluan

Sebagai developer web, kita berinteraksi dengan error setiap hari. Mulai dari data yang null atau undefined, respons API yang gagal, hingga validasi input yang tidak sesuai. Cara kita mengelola error ini sangat menentukan kualitas, stabilitas, dan keterbacaan kode aplikasi kita.

Secara tradisional, kita sering mengandalkan try-catch untuk error sinkron dan Promise .catch() untuk error asinkron. Namun, pendekatan ini terkadang bisa membuat kode menjadi berantakan, dan yang lebih parah, null atau undefined yang tidak ditangani dengan baik bisa menjadi bom waktu yang meledak di runtime tanpa kita sadari.

Bagaimana jika ada cara yang lebih elegan, prediktif, dan fungsional untuk menangani ‘ketidakpastian’ ini? Di sinilah konsep Monads dari dunia Functional Programming (FP) hadir. Jangan takut dengan istilah ‘Monad’ yang sering terdengar rumit. Dalam artikel ini, kita akan membahasnya dengan cara yang praktis dan mudah dipahami, fokus pada dua jenis Monad yang sangat berguna untuk penanganan error: Option (atau Maybe) dan Either.

📌 Tujuannya bukan untuk membuat Anda menjadi ahli matematika, melainkan untuk memberikan ‘alat’ baru yang powerful untuk membangun aplikasi web yang lebih tangguh dan bersih, terutama dengan bantuan kekuatan tipe di TypeScript.

2. Memahami Masalah Penanganan Error Tradisional

Mari kita lihat skenario umum yang sering kita hadapi. Bayangkan Anda memiliki sebuah fungsi yang mengambil data pengguna dari sebuah objek, yang mungkin saja tidak lengkap.

interface User {
  id: string;
  name?: string; // name bisa undefined
  address?: {
    street?: string; // street bisa undefined
    city?: string;
  };
}

function getUserStreet(user: User): string | null {
  // Masalah 1: Cek null/undefined berulang
  if (user && user.address && user.address.street) {
    return user.address.street;
  }
  return null;
}

const user1: User = { id: "1", name: "Alice", address: { street: "Jalan ABC", city: "Jakarta" } };
const user2: User = { id: "2", name: "Bob", address: { city: "Bandung" } };
const user3: User = { id: "3", name: "Charlie" };

console.log(getUserStreet(user1)); // Jalan ABC
console.log(getUserStreet(user2)); // null
console.log(getUserStreet(user3)); // null

Kode di atas sudah ‘defensif’ dengan melakukan cek null/undefined. Namun, bayangkan jika rantainya lebih panjang (user.address.street.zipCode.firstDigit). Kode akan menjadi sangat panjang dan sulit dibaca.

Masalah lain muncul ketika kita lupa melakukan cek:

function printUserStreetUnsafe(user: User) {
  // Potensi Runtime Error jika user.address atau user.address.street undefined
  console.log(user.address.street.toUpperCase());
}

// printUserStreetUnsafe(user2); // TypeError: Cannot read properties of undefined (reading 'toUpperCase')

Ini adalah masalah klasik “billion-dollar mistake” (null reference) yang menyebabkan banyak bug di aplikasi. Monads menawarkan cara yang lebih terstruktur dan eksplisit untuk menangani kasus-kasus di mana nilai bisa saja tidak ada atau operasi bisa gagal.

3. Apa itu Monads (Secara Sederhana)? Analogi Kotak Ajaib

Lupakan sejenak definisi matematika yang rumit. Anggap saja Monad itu seperti “kotak” atau “wadah” yang membungkus sebuah nilai. Kotak ini bukan kotak biasa, melainkan kotak ‘ajaib’ yang memiliki aturan khusus tentang bagaimana Anda bisa berinteraksi dengan nilai di dalamnya, terutama ketika nilai itu mungkin tidak ada atau ketika ada kemungkinan error.

💡 Analogi Kotak: Bayangkan Anda memiliki kotak hadiah.

Monads menyediakan metode untuk ‘memanipulasi’ isi kotak ini tanpa harus selalu ‘mengeluarkan’ isinya secara langsung. Ini membuat flow program lebih aman dan prediktif.

4. Monad Option (atau Maybe): Mengatasi null dan undefined

Monad Option (kadang disebut Maybe) sangat cocok untuk skenario di mana sebuah nilai mungkin ada (Some) atau tidak ada (None). Ini adalah pengganti yang elegan untuk null atau undefined.

Struktur Dasar Option

Sebuah Option<T> bisa jadi:

// Implementasi Option sederhana
class Some<T> {
  constructor(public value: T) {}

  isSome(): this is Some<T> { return true; }
  isNone(): this is None<T> { return false; }

  // map: Terapkan fungsi ke nilai jika ada
  map<U>(f: (value: T) => U): Option<U> {
    return Option.Some(f(this.value));
  }

  // flatMap: Terapkan fungsi yang mengembalikan Option ke nilai jika ada
  // Berguna untuk chaining operasi yang juga bisa mengembalikan Option
  flatMap<U>(f: (value: T) => Option<U>): Option<U> {
    return f(this.value);
  }

  // getOrElse: Dapatkan nilai, atau default jika None
  getOrElse(defaultValue: T): T {
    return this.value;
  }
}

class None<T> {
  isSome(): this is Some<T> { return false; }
  isNone(): this is None<T> { return true; }

  map<U>(f: (value: T) => U): Option<U> {
    return Option.None(); // Jika None, fungsi tidak diterapkan
  }

  flatMap<U>(f: (value: T) => Option<U>): Option<U> {
    return Option.None(); // Jika None, fungsi tidak diterapkan
  }

  getOrElse(defaultValue: T): T {
    return defaultValue; // Jika None, kembalikan default
  }
}

type Option<T> = Some<T> | None<T>;

namespace Option {
  export function Some<T>(value: T): Option<T> {
    return new Some(value);
  }
  export function None<T>(): Option<T> {
    return new None<T>();
  }
  export function fromNullable<T>(value: T | null | undefined): Option<T> {
    return value === null || value === undefined ? Option.None() : Option.Some(value);
  }
}

Menggunakan Option untuk Skenario getUserStreet

Mari kita refaktor fungsi getUserStreet dengan Option.

function getUserStreetOption(user: User): Option<string> {
  return Option.fromNullable(user)
    .flatMap(u => Option.fromNullable(u.address))
    .flatMap(a => Option.fromNullable(a.street));
}

console.log(getUserStreetOption(user1).getOrElse("Tidak ada jalan")); // Jalan ABC
console.log(getUserStreetOption(user2).getOrElse("Tidak ada jalan")); // Tidak ada jalan
console.log(getUserStreetOption(user3).getOrElse("Tidak ada jalan")); // Tidak ada jalan

// Contoh chaining operasi dengan map
const streetLength = getUserStreetOption(user1)
  .map(street => street.length)
  .getOrElse(0);
console.log("Panjang jalan:", streetLength); // Panjang jalan: 11

const streetLengthNone = getUserStreetOption(user3)
  .map(street => street.length)
  .getOrElse(0);
console.log("Panjang jalan (None):", streetLengthNone); // Panjang jalan (None): 0

✅ Manfaat Option:

5. Monad Either: Mengelola Sukses atau Gagal

Monad Either sangat berguna ketika sebuah operasi bisa memiliki dua hasil yang berbeda: sukses (dengan nilai) atau gagal (dengan error). Ini memaksa Anda untuk menangani kedua skenario tersebut secara eksplisit.

Struktur Dasar Either

Sebuah Either<L, R> bisa jadi:

// Implementasi Either sederhana
class Right<L, R> {
  constructor(public value: R) {}

  isRight(): this is Right<L, R> { return true; }
  isLeft(): this is Left<L, R> { return false; }

  map<U>(f: (value: R) => U): Either<L, U> {
    return Either.Right(f(this.value));
  }

  flatMap<U>(f: (value: R) => Either<L, U>): Either<L, U> {
    return f(this.value);
  }

  // fold: Menerapkan satu dari dua fungsi tergantung apakah Left atau Right
  fold<T>(onLeft: (left: L) => T, onRight: (right: R) => T): T {
    return onRight(this.value);
  }
}

class Left<L, R> {
  constructor(public value: L) {}

  isRight(): this is Right<L, R> { return false; }
  isLeft(): this is Left<L, R> { return true; }

  map<U>(f: (value: R) => U): Either<L, U> {
    return Either.Left(this.value); // Jika Left, fungsi tidak diterapkan
  }

  flatMap<U>(f: (value: R) => Either<L, U>): Either<L, U> {
    return Either.Left(this.value); // Jika Left, fungsi tidak diterapkan
  }

  fold<T>(onLeft: (left: L) => T, onRight: (right: R) => T): T {
    return onLeft(this.value);
  }
}

type Either<L, R> = Left<L, R> | Right<L, R>;

namespace Either {
  export function Left<L, R>(value: L): Either<L, R> {
    return new Left(value);
  }
  export function Right<L, R>(value: R): Either<L, R> {
    return new Right(value);
  }
}

Menggunakan Either untuk Validasi Input

Bayangkan Anda ingin memvalidasi usia pengguna.

function validateAge(age: number): Either<string, number> {
  if (isNaN(age) || age < 0) {
    return Either.Left("Usia tidak valid: harus angka positif.");
  }
  if (age < 18) {
    return Either.Left("Usia harus minimal 18 tahun.");
  }
  return Either.Right(age);
}

console.log(validateAge(25).fold(error => `Error: ${error}`, age => `Usia valid: ${age}`)); // Usia valid: 25
console.log(validateAge(16).fold(error => `Error: ${error}`, age => `Usia valid: ${age}`)); // Error: Usia harus minimal 18 tahun.
console.log(validateAge(-5).fold(error => `Error: ${error}`, age => `Usia valid: ${age}`)); // Error: Usia tidak valid: harus angka positif.

Chaining Operasi dengan Either

Anda bisa menggabungkan beberapa operasi yang mengembalikan Either.

function parseAndValidateAge(input: string): Either<string, number> {
  const parsedAge = parseInt(input);
  if (isNaN(parsedAge)) {
    return Either.Left("Input harus berupa angka.");
  }
  return validateAge(parsedAge); // Mengembalikan Either dari validateAge
}

function getEligibilityMessage(inputAge: string): string {
  return parseAndValidateAge(inputAge).fold(
    error => `Gagal memproses: ${error}`,
    age => `Pengguna berusia ${age} tahun dan memenuhi syarat.`
  );
}

console.log(getEligibilityMessage("20"));    // Pengguna berusia 20 tahun dan memenuhi syarat.
console.log(getEligibilityMessage("10"));    // Gagal memproses: Usia harus minimal 18 tahun.
console.log(getEligibilityMessage("abc"));   // Gagal memproses: Input harus berupa angka.

🎯 Manfaat Either:

6. Penerapan Praktis di Aplikasi Web

Validasi Input Formulir