Pemeriksaan Tipe Anda Lolos. Aplikasi Anda Tetap Crash.
Anda mengaktifkan strict: true di tsconfig.json. Anda memperbaiki setiap garis merah bergelombang. Anda merilis ke produksi dengan percaya diri bahwa null dan undefined sudah bukan masalah lagi.
Lalu respons backend berubah bentuk, query DOM mengembalikan nilai kosong, dan user.profile.name melempar Cannot read properties of null di kode persis yang TypeScript katakan aman. Apa yang terjadi?
Pemeriksaan null strict TypeScript adalah kontrak compile-time. Ia memverifikasi bahwa tipe yang Anda tulis konsisten secara internal. Ia tidak memverifikasi bahwa data yang datang saat runtime cocok dengan tipe tersebut. Celah antara kedua hal itu adalah tempat di mana sebagian besar crash null di produksi terjadi.
Di Mana strictNullChecks Sebenarnya Membantu
Ketika Anda mengaktifkan strictNullChecks, TypeScript memperlakukan null dan undefined sebagai tipe yang berbeda yang harus ditangani secara eksplisit.
// Without strictNullChecks, this compiles. With it, you get a type error.
function greet(name: string) {
console.log(name.toUpperCase())
}
greet(null) // Error: Argument of type 'null' is not assignable to parameter of type 'string'.
Ini benar-benar berguna. Ia menangkap null yang Anda perkenalkan di kode Anda sendiri: variabel yang belum diinisialisasi, nilai return yang hilang, kasus default yang terlupakan.
Masalahnya adalah ini hanya bekerja pada kode yang TypeScript bisa lihat saat compile time. Setelah aplikasi Anda berjalan, TypeScript sudah tidak ada. Sistem tipe menghilang. Yang tersisa adalah JavaScript biasa, dan JavaScript biasa dengan senang hati akan membiarkan null mengalir melalui setiap lubang yang Anda tinggalkan terbuka.
Empat Celah Runtime yang Tidak Bisa Ditutup strictNullChecks
1. Respons API Berpura-pura Cocok dengan Tipe Anda
Anda mengetik respons API Anda. TypeScript mempercayai Anda.
interface User {
id: number
profile: {
name: string
avatar: string
}
}
async function fetchUser(): Promise<User> {
const res = await fetch('/api/user')
return res.json() as User // TypeScript trusts this cast. The backend does not.
}
const user = await fetchUser()
console.log(user.profile.name) // Crashes if profile is null
Asersi as User memberitahu TypeScript untuk berhenti bertanya. Ia tidak memvalidasi payload. Jika backend mengembalikan { id: 1, profile: null }, TypeScript mengkompilasi kode Anda berdasarkan kebohongan. Crash-nya nyata.
2. Query DOM Mengembalikan null Secara Desain
const button = document.getElementById('submit')
button.addEventListener('click', handleClick) // Crashes if the element is missing
TypeScript tahu getElementById mengembalikan HTMLElement | null. Dalam mode strict, ini memaksa Anda untuk menangani kasus null. Banyak developer mengakalinya dengan non-null assertion:
const button = document.getElementById('submit')!
Tanda ! adalah janji kepada TypeScript bahwa Anda lebih tahu. Ketika Anda salah, crash-nya mendarat di produksi.
3. noUncheckedIndexedAccess Mati Secara Default
Bahkan dengan strict: true, TypeScript tidak menandai akses index array atau objek sebagai kemungkinan undefined.
const users: User[] = []
const first = users[0]
first.name // No type error, but first is undefined at runtime
Hal yang sama berlaku untuk records:
const cache: Record<string, string> = {}
const value = cache['missing'] // Type: string. Runtime value: undefined.
Anda memerlukan noUncheckedIndexedAccess: true di tsconfig.json Anda untuk menutup lubang ini. Sebagian besar tim tidak pernah mengaktifkannya karena membuat kode sehari-hari lebih berisik. Keributan itulah intinya.
4. any dan Type Assertion Menonaktifkan Segalanya
const data: any = JSON.parse(raw)
const user = data.user as User // All null checks are now voluntarily disabled
Setiap any atau as di codebase Anda adalah pintu yang Anda tinggalkan tidak terkunci. TypeScript berhenti di ambang pintu. Apa yang masuk melaluinya adalah tanggung jawab Anda.
Apa yang Sebenarnya Berfungsi saat Runtime
Tugas TypeScript adalah menangkap kesalahan di kode yang Anda tulis. Keamanan runtime memerlukan lapisan pertahanan kedua.
Validasi di Boundary
Gunakan library schema seperti Zod untuk memvalidasi data eksternal sebelum memasuki sistem tipe Anda.
import { z } from 'zod'
const UserSchema = z.object({
id: z.number(),
profile: z.object({
name: z.string(),
avatar: z.string().optional(),
}).nullable(),
})
const res = await fetch('/api/user')
const raw = await res.json()
const user = UserSchema.parse(raw) // Throws if the shape is wrong
// user.profile is typed as { name: string; avatar?: string } | null
if (user.profile) {
console.log(user.profile.name) // Safe
}
Biayanya adalah langkah validasi runtime di setiap boundary. Manfaatnya adalah tipe Anda tidak lagi menjadi angan-angan. Mereka ditegakkan.
Gunakan noUncheckedIndexedAccess
Aktifkan. Terima friksinya.
// With noUncheckedIndexedAccess
const first = users[0] // Type: User | undefined
if (first) {
console.log(first.name) // Safe
}
Ya, ini menambahkan pemeriksaan di mana pun Anda mengakses index. Itulah persis yang Anda inginkan jika Anda serius tentang null safety.
Defaultkan ke Optional Chaining, Bukan Non-Null Assertion
// Dangerous
const name = user.profile!.name
// Safe
const name = user.profile?.name ?? 'Anonymous'
Optional chaining melakukan short-circuit pada null atau undefined. Operator nullish coalescing menyediakan fallback. Pola ini bersifat defensif secara default dan eksplisit tentang apa yang terjadi ketika data hilang.
Jauhkan any dari Alur Data Anda
Perlakukan any seperti tumpahan beracun. Jika Anda harus menggunakannya, batasi ke scope sekecil mungkin dan validasi outputnya segera.
// Bad: any propagates
const data: any = JSON.parse(raw)
processData(data)
// Better: validate and narrow immediately
const parsed = JSON.parse(raw) as unknown
const data = UserSchema.parse(parsed)
processData(data)
unknown memaksa Anda untuk membuktikan bentuknya sebelum TypeScript mengizinkan Anda menggunakannya. Itu adalah any dengan konsekuensi.
Mengapa Tim Melewatkan Pertahanan Ini
Sebagian besar tim tidak melewatkannya karena ketidaktahuan. Mereka melewatkannya karena pola yang lebih aman menambah friksi pada setiap akses data, setiap pemanggilan API, setiap query DOM. Kodenya jadi lebih panjang. Tipe-tipe jadi lebih berisik. Godaan untuk menempelkan ! atau as pada masalah tumbuh bersama setiap deadline.
Alternatifnya adalah crash di produksi yang TypeScript janjikan tidak akan Anda alami. Compiler menepati bagiannya dari kesepakatan. Ia memeriksa kode Anda terhadap tipe yang Anda berikan. Ia tidak bisa memeriksa kode Anda terhadap kenyataan.
Perbaikannya Adalah Dua Lapisan, Bukan Satu
strictNullChecks adalah lapisan satu. Ia menangkap null yang Anda perkenalkan sendiri. Lapisan dua adalah segalanya yang lain: schema validation, defensive indexing, optional chaining, dan disiplin untuk menghindari any.
Jika aplikasi TypeScript Anda masih crash karena null, compiler tidak gagal pada Anda. Celah antara tipe Anda dan data runtime Anda yang gagal. Tutuplah celah itu di boundary, aktifkan noUncheckedIndexedAccess, dan berhentilah mempercayai backend untuk cocok dengan interface Anda. TypeScript akan tetap strict. Tapi setidaknya tipe Anda akan menggambarkan dunia sebagaimana datangnya.