Pola TypeScript Lanjutan untuk Aplikasi Produksi

Eksplorasi pola TypeScript tingkat lanjutan yang akan membuat kode kamu lebih aman, fleksibel, dan mudah dipertahankan.

AA

Abiyyu Abidiffatir Al Majid

5 menit baca
Pola TypeScript Lanjutan untuk Aplikasi Produksi

Mengapa TypeScript Lanjutan?

TypeScript sudah jauh melampaui "JavaScript dengan tipe." Fitur-fitur lanjutan seperti conditional types, template literal types, dan mapped types memungkinkan kamu untuk mengekspresikan invariant bisnis secara langsung di level tipe — artinya kesalahan tertangkap saat compile, bukan saat runtime.

Artikel ini membahas pola-pola yang sering saya gunakan di kodebase produksi.

Discriminated Unions

Pola paling fundamental untuk memodelkan state yang eksklusif. Alih-alih menggunakan boolean flag yang bisa konflik, gunakan union type dengan tag eksplisit:

// ❌ Rentan terhadap state tidak valid
interface RequestState {
  isLoading: boolean
  isError: boolean
  data?: User[]
  error?: Error
}
// isLoading: true, isError: true — state ini tidak masuk akal!

// ✅ Discriminated union — setiap state eksklusif type RequestState<T> = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }

function handleState(state: RequestState<User[]>) { switch (state.status) { case 'idle': return 'Silakan mulai pencarian' case 'loading': return 'Memuat...' case 'success': return ${state.data.length} pengguna ditemukan case 'error': return Error: ${state.error.message} } }

Keuntungan: TypeScript tahu bahwa data pasti ada saat status === 'success' — tidak perlu optional chaining atau type assertion.

Template Literal Types

Template literal types memungkinkan kamu membangun tipe string yang kompleks secara deklaratif:

// Event system yang type-safe
type EventName = 'click' | 'hover' | 'focus'
type EventHandler = on${Capitalize<EventName>}

// Hasil: 'onClick' | 'onHover' | 'onFocus'

// Lebih powerful: constraining object keys type PropEventSource<T> = { [K in keyof T as on${Capitalize<string & K>}Change]?: ( newValue: T[K] ) => void }

interface UserForm { name: string age: number email: string }

// Hasilnya type-safe: type UserFormEvents = PropEventSource<UserForm> // { // onNameChange?: (newValue: string) => void // onAgeChange?: (newValue: number) => void // onEmailChange?: (newValue: string) => void // }

function createUserFormEvents(): UserFormEvents { return { onNameChange: (name) => console.log(Nama berubah: ${name}), onAgeChange: (age) => console.log(Umur berubah: ${age}), } }

Conditional Types

Conditional types memungkinkan tipe berubah berdasarkan kondisi, sangat berguna untuk utility functions:

// Tipe return yang adaptif berdasarkan input
type ApiResponse<T extends 'user' | 'product'> =
  T extends 'user'
    ? { id: string; name: string; email: string }
    : T extends 'product'
      ? { id: string; title: string; price: number }
      : never

async function fetchData<T extends 'user' | 'product'>( resource: T ): Promise<ApiResponse<T>> { const response = await fetch(/api/${resource}) return response.json() }

// TypeScript tahu return type-nya! const user = await fetchData('user') // user: { id: string; name: string; email: string } console.log(user.name) // ✅ console.log(user.price) // ❌ Error: Property 'price' does not exist

Advanced Utility Types

DeepPartial

Membuat semua property nested menjadi optional:

type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T

interface Config { database: { host: string port: number credentials: { username: string password: string } } cache: { ttl: number } }

// Semua nested property jadi optional type PartialConfig = DeepPartial<Config>

const config: PartialConfig = { database: { credentials: { username: 'admin', // password bisa di-skip karena optional }, }, }

StrictOmit dengan Validasi Key

Membuat Omit yang menolak key yang tidak ada di tipe asal:

type StrictOmit<T, K extends keyof T> = Omit<T, K>

interface User { id: string name: string email: string }

// ✅ Ini valid type CreateUserInput = StrictOmit<User, 'id'>

// ❌ Error: Argument of type '"age"' is not assignable type Wrong = StrictOmit<User, 'age'>

Branded Types

Mencegat tipe primitif yang secara struktural identik tapi secara semantik berbeda:

type Brand<T, B extends string> = T & { readonly __brand: B }

type UserId = Brand<string, 'UserId'> type OrderId = Brand<string, 'OrderId'>

function getUser(id: UserId) { / ... / } function getOrder(id: OrderId) { / ... / }

const userId = 'abc-123' as UserId const orderId = 'xyz-789' as OrderId

getUser(userId) // ✅ getUser(orderId) // ❌ Error! Type 'OrderId' is not assignable to type 'UserId'

Exhaustive Checking

Pastikan switch/case menangani semua kemungkinan:

function assertNever(value: never): never {
  throw new Error(Unexpected value: ${value})
}

type Status = 'pending' | 'active' | 'suspended'

function getStatusColor(status: Status): string { switch (status) { case 'pending': return 'yellow' case 'active': return 'green' case 'suspended': return 'red' default: return assertNever(status) } } // Jika ada status baru ditambahkan tapi belum di-handle, // TypeScript akan error di compile time!

Kesimpulan

TypeScript yang powerful bukan tentang menggunakan semua fitur yang ada — tapi tentang memilih pola yang tepat untuk mengekspresikan invariant bisnis kamu. Discriminated unions untuk state management, template literal types untuk API yang konsisten, conditional types untuk generic yang adaptif, dan branded types untuk mencegah kesalahan konversi tipe.

Mulai dari yang paling berdampak: konversi boolean flags ke discriminated unions. Perubahan ini saja sudah cukup untuk menghilangkan banyak bug yang biasanya hanya ketahuan di runtime.

#TypeScript#Design Patterns#Type Safety#Web Development
AA

Dibuat oleh

Abiyyu Abidiffatir Al Majid

Software Engineer passionate about building scalable web applications and sharing knowledge about modern web development, system design, and emerging technologies.

Artikel Terkait