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.
Dibuat oleh
Abiyyu Abidiffatir Al MajidSoftware Engineer passionate about building scalable web applications and sharing knowledge about modern web development, system design, and emerging technologies.



