⚡ Jawaban Singkat / Key Takeaways: TypeScript 5.5 memperketat exhaustiveness checking lewat never type fallback pada discriminated union. Kalau kamu lupa menangani satu varian state, compiler akan menolak kode-mu sebelum sempat menyentuh production. Tapi hati-hati: union reduction di TypeScript bisa “menyembunyikan” varian yang seharusnya eksis, bikin pengecekan tampak lengkap padahal bolong.
Debug State Machine yang Bikin Kamu Bangun Tengah Malam
Jam 2 pagi. Kamu terbangun oleh Slack alert: production error di payment flow. Stack trace menunjukkan Cannot read properties of undefined di reducer Redux. Setelah tiga jam debugging, kamu sadar: ada satu action type baru yang lupa ditangani. Satu varian. Satu state. Dan aplikasi-mu crash.
Ini bukan cerita fiksi. Library author, TypeScript power user, dan developer yang membangun type-safe state machine pasti pernah mengalami lubang ini. Pattern matching di TypeScript memang kuat, tetapi tanpa exhaustiveness checking yang benar, discriminated union hanya memberi ilusi keamanan, bukan jaminan nyata.
Nah, TypeScript 5.5 membawa penyempurnaan penting di area ini. Mari kita bongkar bagaimana never type berperan sebagai penjaga terakhir, sekaligus mengungkap edge cases yang bisa menjebak kamu.
Kenapa Exhaustiveness Checking Jadi Senjata Rahasia Library Author
Bayangkan kamu sedang mendesain tipe untuk state machine payment gateway:
type PaymentState =
| { status: 'idle' }
| { status: 'processing'; transactionId: string }
| { status: 'success'; receiptUrl: string }
| { status: 'failed'; errorCode: number };
Sekarang, kamu menulis fungsi handlePayment yang melakukan pattern matching terhadap PaymentState. Kalau kamu cuma menangani tiga dari empat varian, compiler TypeScript biasa tidak akan berteriak. Hasilnya? Runtime crash ketika status 'failed' muncul di production.
Di sinilah exhaustiveness checking berperan. Dengan menambahkan never sebagai fallback di default branch, kamu memaksa TypeScript memverifikasi bahwa semua varian sudah ditangani. Kalau ada yang terlewat, kode gagal compile. Bukan gagal di production.
function handlePayment(state: PaymentState): string {
switch (state.status) {
case 'idle':
return 'Menunggu pembayaran';
case 'processing':
return `Memproses transaksi ${state.transactionId}`;
case 'success':
return `Struk: ${state.receiptUrl}`;
case 'failed':
return `Gagal dengan kode ${state.errorCode}`;
default:
// TypeScript: jika semua varian sudah ditangani,
// state di sini bertipe `never`
const _exhaustive: never = state;
return _exhaustive;
}
}
Teknik ini bukan sekadar trik kode. Ini adalah soundness guarantee di level type system. Kamu mengubah runtime bug menjadi compile-time error. Dan itu perbedaan antara bangun jam 2 pagi dengan tidur nyenyak.
Mekanisme Never Type: Bukan Sekadar Bottom Type Biasa
Banyak developer menganggap never sama seperti void atau undefined. Ini pemahaman yang keliru dan berbahaya. never adalah bottom type: tipe yang tidak memiliki nilai sama sekali. Dalam teori himpunan, never adalah himpunan kosong.
Kekuatan never muncul justru karena ketidakmungkinannya. Ketika TypeScript mempersempit (narrow) union melalui pattern matching, branch terakhir yang tidak terjangkau akan memiliki tipe never. Assign never ke variabel bertipe never selalu valid karena “tidak mungkin terjadi.” Tapi begitu ada varian union yang belum ditangani, tipe di default branch bukan lagi never, melainkan varian yang tersisa. Assignment gagal. Compile error. Aman.
Pola ini sangat berguna untuk library author yang mendesain API publik. Misalnya, library @your-org/state-machine yang menyediakan tipe Transition<S, E>. Dengan never fallback, konsumen library-mu akan langsung tahu saat mereka upgrade ke versi baru yang menambah varian state. Tidak perlu menunggu bug report.
Jebakan Union Reduction yang Bikin Pattern Matching-mu Bolong
Sekarang masuk ke bagian yang jarang dibahas: union reduction. TypeScript secara agresif menyederhanakan union type di internal. Dalam banyak kasus, ini membantu performa dan readability. Tapi ada situasi di mana reduksi ini justru menghilangkan informasi penting dan membuat exhaustiveness checking terlihat lengkap padahal tidak.
Perhatikan contoh berikut:
type A = { kind: 'a'; value: string };
type B = { kind: 'b'; value: string };
type C = { kind: 'c'; value: string };
// Union reduction: TypeScript melihat B dan C identik secara struktural
// (keduanya punya kind literal berbeda, jadi aman... atau tidak?)
type ABC = A | B | C;
Masalah sesungguhnya muncul saat kamu menggunakan template literal types atau conditional types yang kompleks. TypeScript bisa mereduksi union dengan cara yang tidak terduga. Misalnya, ketika dua varian berbeda menghasilkan tipe yang sama setelah mapped type diterapkan, exhaustiveness checking bisa memberikan sinyal “semua aman” padahal secara logika masih ada varian yang belum tertangani.
Contoh konkret: bayangkan kamu sedang membangun type-safe state machine untuk editor teks dengan mode insert, visual, normal, dan command. Masing-masing mode memiliki sub-state yang berbeda. Jika kamu menggunakan union reduction secara tidak hati-hati lewat Exclude, Extract, atau distribusi conditional type, beberapa kombinasi state bisa “lenyap” dari union hasil.
type EditorMode = 'insert' | 'visual' | 'normal' | 'command';
// Hati-hati: distribusi conditional type bisa mereduksi union
// secara tidak terduga saat T extends never terpicu
type ActiveModes<T> = T extends 'insert' | 'visual' ? T : never;
// Hasil: 'insert' | 'visual'
// Tapi 'command' hilang! Dan TypeScript tidak memperingatkan.
Solusinya? Selalu periksa hasil union dengan type-level assert atau unit test tipe menggunakan expect-type (library dari DefinitelyTyped). Ini memastikan bahwa union yang kamu oper ke fungsi pattern matching benar-benar lengkap, bukan hasil reduksi yang sudah kehilangan varian.
State Machine Type-Safe: Dari Reducer Redux sampai FSM Kompleks
Redux adalah contoh klasik di mana exhaustiveness checking bisa menyelamatkan hidupmu. Setiap kali kamu menambah action type baru, semua reducer yang menangani union action tersebut harus diperbarui. Tanpa never fallback, kamu mengandalkan disiplin manual. Dan kita semua tahu disiplin manual tidak berskala.
Berikut pola yang bisa kamu adopsi untuk Redux Toolkit atau Zustand middleware:
type CounterAction =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'RESET'; payload: number };
function counterReducer(state: number, action: CounterAction): number {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
case 'RESET':
return action.payload;
default: {
const _exhaustive: never = action;
throw new Error(`Unhandled action: ${JSON.stringify(_exhaustive)}`);
}
}
}
Tapi FSM (Finite State Machine) modern jauh lebih kompleks daripada reducer counter. Library seperti XState menggunakan discriminated union dengan nested state. Di sinilah exhaustiveness checking TypeScript 5.5 benar-benar bersinar. Kamu bisa memverifikasi tidak hanya state level atas, tetapi juga sub-state dan transisi antar state.
Untuk kamu yang sudah membaca artikel tentang framework TypeScript terbaik 2026, kamu pasti paham bahwa framework modern seperti tRPC dan NestJS sangat bergantung pada type safety di layer foundational. Exhaustiveness checking adalah salah satu fondasi itu.
Tiga Atribut yang Bikin Never Fallback Berhasil
Setelah mendesain puluhan type-safe API untuk library production, saya mengidentifikasi tiga atribut kunci yang bikin never fallback bekerja dengan benar:
- Discriminated union wajib punya discriminant literal. Tanpa properti
kind,status, atautypeyang bertipe literal string/number, narrowing TypeScript tidak cukup presisi. Gunakanas constpada objek discriminant kamu. - Hindari
anydi sekitar union. Satuanykecil di tengah rantai type inference bisa menghancurkan seluruh exhaustiveness checking. Gunakan TypeScript narrowing secara eksplisit. - Validasi union completeness di level compile-time. Jangan hanya mengandalkan switch statement. Gunakan teknik saturasi union: jika kamu menulis
type AllStates = PaymentState['status'], pastikan jumlah literal member sesuai ekspektasi. Referensi: TypeScript 5.5 release notes.
Kesimpulan
Exhaustiveness checking dengan never type bukan lagi fitur opsional untuk library author. Ini adalah safety net yang memisahkan kode produksi yang tangguh dari kode yang rapuh. TypeScript 5.5 membawa penyempurnaan yang membuat pengecekan ini makin ketat, tapi tanggung jawab tetap ada di tangan kamu sebagai developer: pahami union reduction, validasi tipe di level compile-time, dan jangan pernah percaya bahwa “rasanya semua state sudah ditangani.”
Coba audit kode state machine-mu sekarang. Cari switch statement atau if-else chain yang menangani discriminated union. Tambahkan never fallback. Lihat apakah TypeScript berteriak. Kalau iya, kamu baru saja mencegah bug production berikutnya.
Mau insight advanced TypeScript kayak gini langsung ke inbox? Subscribe newsletter Hadezuka. Nggak spam. Cuma konten yang bener-bener berguna buat developer kayak kamu.
FAQ
void berarti fungsi tidak mengembalikan nilai yang berguna (undefined). never berarti fungsi tidak akan pernah selesai dieksekusi, misalnya throw error atau infinite loop. never adalah bottom type, artinya tidak ada nilai yang bisa di-assign ke never. Inilah yang bikin never ideal untuk exhaustiveness checking.
Tidak. Kamu juga bisa pakai if-else chain, objek lookup dengan Record, atau pattern matching library seperti ts-pattern. Yang penting, ada branch akhir yang meng-assign ke never untuk memverifikasi semua varian sudah ditangani.
TypeScript secara internal menyederhanakan union yang dianggap redundant. Misalnya, A | never langsung jadi A. Atau dua tipe objek dengan struktur identik bisa di-merge. Kalau varian yang hilang itu penting secara domain, compiler tidak akan memperingatkan karena secara struktural union tetap sound. Solusinya: gunakan brand type atau discriminant literal unik untuk mencegah merge.
XState v5 punya dukungan TypeScript yang sangat baik, tapi exhaustiveness checking tetap bergantung pada cara kamu mengonsumsi tipe dari XState. Kamu tetap perlu menulis never fallback di handler transisi untuk memastikan semua state dan event tertangani. XState menyediakan tipe, tapi verifikasi kelengkapan tetap tanggung jawab kode kamu.
