karawaci.kode

← Semua snippet

TypeScript Menengah Utility

Discriminated union untuk API response (TypeScript)

Pattern Result<Success, Error> dengan discriminated union. Compile-time exhaustiveness check. Lebih aman daripada try/catch atau {data, error} pattern.

Dipublikasikan 30 Mei 2026

Saya pakai pattern ini di setiap project TS sekarang. Replace try/catch yang verbose dan {data, error} yang ambiguous (kalau both falsy?).

Kode

// types/result.ts
export type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });

// Helper untuk wrap throwing function
export async function tryCatch<T>(
  fn: () => Promise<T> | T,
): Promise<Result<T, Error>> {
  try {
    const value = await fn();
    return ok(value);
  } catch (e) {
    return err(e instanceof Error ? e : new Error(String(e)));
  }
}

Pemakaian: fetch dari API

import { Result, ok, err, tryCatch } from './types/result';

async function getUser(id: string): Promise<Result<User, 'not_found' | 'network'>> {
  try {
    const res = await fetch(`/api/users/${id}`);
    
    if (res.status === 404) return err('not_found');
    if (!res.ok) return err('network');
    
    const user = await res.json();
    return ok(user);
  } catch (e) {
    return err('network');
  }
}

// Pemakaian di controller
const result = await getUser('user-123');

if (result.ok) {
  console.log(result.value.name); // TypeScript tau result.value adalah User
} else {
  // TypeScript tau result.error adalah 'not_found' | 'network'
  switch (result.error) {
    case 'not_found':
      return render404();
    case 'network':
      return renderRetryButton();
    // Exhaustiveness: TypeScript error kalau ada case yang miss
  }
}

Kapan dipakai

  • API endpoints dengan typed errors
  • Form submission yang punya banyak error case
  • File operations (read/write/delete) yang bisa fail multi-cara
  • DB query yang return “not found” yang valid (bukan exception)

Keuntungan vs try/catch

try/catchResult type
Error type tidak typed (catch (e) — e: any)Error type explicit di Result<T, E>
Easy to forget catchCompiler enforce handling
Stack trace at throw siteError sebagai value, lebih predictable
.then().catch() chain susahif (result.ok) straightforward

Catatan

  • E = Error sebagai default supaya kalau gak peduli typed errors, tetap bisa pakai.
  • tryCatch helper: wrap legacy throwing code, return Result.
  • Exhaustiveness check: pakai switch + default: const _: never = result.error untuk compile-time check.

Variasi: explicit error class

Kalau Anda butuh error info lebih detail:

class ApiError {
  constructor(
    public type: 'not_found' | 'unauthorized' | 'validation' | 'server',
    public message: string,
    public status: number,
    public details?: unknown,
  ) {}
}

async function getUser(id: string): Promise<Result<User, ApiError>> {
  // ...
  if (res.status === 404) {
    return err(new ApiError('not_found', `User ${id} tidak ditemukan`, 404));
  }
  // ...
}

Pattern ini dipopulerkan oleh Rust (Result<T, E>) dan TC39 proposal Promise.try. Banyak library TS modern (zod, neverthrow) adopt pattern ini.

# tags

typescriptresult-typeerror-handlingpattern

Ditulis oleh Asti Larasati · 30 Mei 2026