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/catch | Result type |
|---|---|
| Error type tidak typed (catch (e) — e: any) | Error type explicit di Result<T, E> |
| Easy to forget catch | Compiler enforce handling |
| Stack trace at throw site | Error sebagai value, lebih predictable |
.then().catch() chain susah | if (result.ok) straightforward |
Catatan
E = Errorsebagai default supaya kalau gak peduli typed errors, tetap bisa pakai.tryCatchhelper: wrap legacy throwing code, return Result.- Exhaustiveness check: pakai
switch+default: const _: never = result.erroruntuk 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 proposalPromise.try. Banyak library TS modern (zod, neverthrow) adopt pattern ini.
# tags
typescriptresult-typeerror-handlingpattern
Ditulis oleh Asti Larasati · 30 Mei 2026