π Topik β Type Systems: static vs dynamic, soundness, dan dampaknya ke bug produksi
Sesi: Deep Learning 05:00 Β· Level: intermediateβadvanced Β· Kategori: CS Fundamentals
π― Tujuan Belajar
Setelah sesi ini, keputusan berikut jadi lebih baik:
- Memilih bahasa/tipe strategi per komponen (bukan per proyek): di mana type checker memberi leverage, di mana ia hanya menambah gesekan.
- Menilai klaim "types prevent bugs" secara kuantitatif, bukan slogan.
- Tahu persis lubang di type system yang dipakai (TS
any,as, erasure, bivariance) dan menutupnya dengan boundary validation β bukan dengan harapan. - Merancang migrasi bertahap dari untyped β typed (JSβTS, Pythonβmypy) tanpa big-bang rewrite.
π§ Penjelasan Inti
Static vs dynamic = KAPAN tipe diperiksa, bukan SEBERAPA KETAT.
Dua sumbu berbeda, sering dicampur:
| Sumbu | Nilai | Contoh |
|---|---|---|
| Waktu pemeriksaan | static (compile-time) vs dynamic (runtime) | Java/Go/Rust vs Python/Ruby/JS |
| Kekuatan (strength) | strong (sedikit coercions implisit) vs weak (banyak) | Python strong, JS weak, PHP weak-ish |
| Soundness | type system menjamin "no stuck program" atau tidak | Rust (βsound, minus unsafe), TS (explicitly unsound), Java (unsound via array covariance + null) |
Kenapa ada type system? Type = bukti parsial tentang bentuk data yang dijamin compiler, sehingga error kelas tertentu tidak bisa lolos ke runtime. Nilainya bukan "mencegah semua bug" β nilainya menggeser deteksi bug dari produksi ke editor, dengan biaya: anotasi, kompilasi, dan menolak program yang sebenarnya valid.
Soundness didefinisikan formal lewat dua teorema: progress (program well-typed tidak macet) + preservation (tipe tetap valid setelah step evaluasi). Konsekuensi praktis: soundness adalah properti all-or-nothing. Satu lubang (unsafe, any, downcast tanpa check) menghapus jaminan global untuk jalur yang melewatinya. Type system yang "99% sound" secara matematis sama dengan tidak sound β hanya secara statistik lebih baik.
Angka, bukan retorika (DIKETAHUI): - Gao, Bird, Barr (2017), To Type or Not to Type: Quantifying Detectable Bugs in JavaScript β menganalisis bug nyata di repo GitHub publik: ~15% bug dapat dideteksi Flow/TypeScript (subset yang bisa di-encode statis). Sisa 85% = logika, spesifikasi, state, integrasi. - Hanenberg (2010), eksperimen 49 subjek, task parser, 27 jam β type statis tidak mempercepat; pada beberapa task justru lebih lambat. Sampel kecil, task tunggal β jangan digeneralisasi; intinya: klaim "types = 2x velocity" tanpa bukti. - Takikawa et al. (2016), Is Sound Gradual Typing Dead? β sound gradual typing (Reticulated Python) bisa menambah overhead runtime hingga ~100%+ pada worst case, terutama untuk boundary checks di kode yang sering dipanggil lintas typed/untyped. Inferensi: inilah alasan praktis kenapa TS memilih unsound-by-design (Design Goal non-goal resmi TS: "apply a sound or provably correct type system" β ditolak demi produktivitas).
βοΈ Cara Kerja Internal
1. Static checking pipeline. Parse β build symbol table β inference (HindleyβMilner untuk ML/Haskell; bidirectional checking untuk TS/Kotlin/Swift) β constraint solving β error report. Yang penting: inferensi bekerja lokal per expression, jadi tipe fungsi lintas modul harus eksplisit atau di-infer dari semua call site (TS butuh deklarasi .d.ts, Go tanpa generics lama butuh duplikasi).
2. Erasure. TS, Java (generics), Python typing, Kotlin β tipe hilang setelah compile/bytecode. Runtime tidak tahu tipe generik. Konsekuensi: List<String> dan List<Int> sama saat runtime (JVM type erasure), JSON.parse() hasilnya any walau kamu tulis as User.
3. Variance β sumber unsoundness klasik.
Java: String[] IS-A Object[] (covariant, unsound)
Object[] a = new String[1];
a[0] = 42; // compile OK, runtime ArrayStoreException
TS: method params bivariant (kecuali strictFunctionTypes utk function props)
Rust: &'a T covariant, &mut T invariant, fn(T) contravariant
Kotlin: out T covariant, in T contravariant, deklarasi-site
4. Structural vs nominal. TS structural (bebek: bentuk cocok = cocok) β mudah komposisi, tapi mudah juga "kebetulan cocok" (dua tipe berbeda bentuk identik saling tertukar, misal UserId vs OrderId keduanya string). Nominal (Java/Rust/C#) butuh explicit implement β aman, tapi lebih kaku. Solusi TS: branded/opaque type:
type UserId = string & { readonly __brand: 'UserId' }
5. Nullability sebagai bagian type system. strictNullChecks/Kotlin T?/Rust Option<T> memindahkan null dari "nilai ajaib di semua tipe" ke "tipe eksplisit yang harus di-destructure". Ini satu perubahan dengan ROI bug tertinggi di migrasi TS (Tony Hoare: null = "billion dollar mistake").
6. Making illegal states unrepresentable. Bukan soal anotasi, soal desain tipe:
// jelek: 4 state valid dari 8 kombinasi
type Req = { loading: boolean; data?: User; error?: string }
// baik: 3 state, exhaustive via switch β compiler paksa handle semua
type Req =
| { s: 'idle' }
| { s: 'ok'; data: User }
| { s: 'err'; error: string }
Discriminated union + exhaustive check = bug "lupa handle error state" jadi compile error, bukan incident.
ποΈ Diagram β boundary model (pola yang dipakai di produksi)
[ wire: JSON / DB row / HTTP header ] β UNTYPED, adversarial
β
parse & validate β zod / io-ts / pydantic / serde
β (satu-satunya tempat runtime check)
βΌ
[ domain type: User, Money, UserId ] β TRUSTED, typed, invariant dijaga
β
pure business logic β no re-validation, no `any`
βΌ
serialize (type-driven encoder)
Aturan: tipe hanya bernilai kalau ada satu titik di mana data tak bertipe dipaksa jadi bertipe lewat validasi runtime. Tanpa itu, anotasi cuma komentar yang tidak bisa dibaca manusia lain.
βοΈ Trade-off & Kapan JANGAN Dipakai
| Dimensi | Static | Dynamic |
|---|---|---|
| Bug tertangkap | sebelum runtime, kelas terbatas (~15% empiris) | setelah deploy, tapi cakupan kelas lebih luas (butuh test) |
| Velocity awal | lebih lambat (anotasi, lawan compiler) | lebih cepat untuk spike/prototipe |
| Refactor skala besar | aman, mekanis, bisa otomatis | berisiko, butuh test coverage tinggi sebagai jaring |
| Tooling | jump-to-def, autocomplete akurat, rename aman | bergantung runtime introspection, lebih lemah |
| Fleksibilitas meta-programming | sering butuh escape hatch (unsafe, any, reflection) |
native |
| Biaya | anotasi + compile time + generics gymnastics | test suite lebih besar + bug di produksi |
Kapan JANGAN pakai type statis ketat:
- Spike/prototipe < 1 hari yang akan dibuang. Anotasi = biaya tanpa payoff.
- EDA / data shape belum diketahui β pandas/polars tanpa schema; paksa tipe di sini = menebak tipe bisnis yang belum ada.
- Plugin system / DSL / interpreter di mana "tipe" baru muncul saat runtime dari config user β butuh validasi runtime, bukan type system.
- Hot path yang di-benchmark dengan sound gradual typing overhead β lebih baik untyped core + typed edge.
- Tim kecil tanpa tooling disiplin: TS dengan strict: false + any di mana-mana = lebih buruk dari JS, karena memberi rasa aman palsu.
Kapan WAJIB pakai (jangan ditawar):
- Kontrak lintas tim/service (API schema, event schema) β protobuf, OpenAPI, zod schema.
- Uang, tanggal/waktu, ID entitas β branded types mencegah OrderId β UserId tertukar.
- State machine domain (order lifecycle, payment status) β sum types + exhaustive check.
- Kode yang bertahan > 6 bulan dan dipegang > 1 orang.
π Contoh Produksi & Failure Mode
-
assebagai kebohongan.const user = await res.json() as Userβ compile bersih,user.address.cityβTypeError: Cannot read properties of undefineddi produksi. TS tidak memvalidasi runtime;ashanya mematikan compiler. Fix:userSchema.parse(await res.json()). -
API drift tanpa kontrak. Backend rename
user_nameβusername; TS interface manual tidak ikut berubah. Tidak ada compile error karena tidak ada shared schema. Fix: codegen dari OpenAPI/protobuf/GraphQL, atau satu package schema bersama. -
Generics erasure menelan bug. Java:
List<Order>di-cast keListlalu isiString; error muncul jauh dari sumber. Fix: jangan raw type, gunakanClass<T>token untuk validasi runtime bila perlu. -
Type-unsoundness tersembunyi di library. Java array covariance, TS method bivariance (callback
(e: MouseEvent) => voidlolos sebagai(e: Event) => void). Bug hanya muncul saat event yang salah dikirim. -
anymenyebar seperti wabah. Satuanydi boundary β semua konsumennya kehilangan tipe. ESLintno-explicit-any,no-unsafe-*ditypescript-eslintjadi kontrol wajib. -
Python: mypy hijau, runtime meledak.
def f(x: int)dipanggil dengan"3"dari kode yang tidak di-type-check (dependency tanpa stub, atau# type: ignore). Fix:disallow_untyped_defs,warn_unused_ignores, cek di CI semua modul. -
Schema drift DB β ORM. Kolom
nullabledi DB, non-null di model β crash saat data lama masuk. Type system tidak tahu DB constraint. Fix: generate tipe dari schema DB (prisma,sqlc,pgschema), satu sumber kebenaran.
β οΈ Miskonsepsi Umum
- β "Static typing = aman." β TS unsound; Java punya null + array covariance. Soundness β static.
- β "Dynamic typing = tidak ada tipe." β Ada, hanya diperiksa runtime; dan banyak bahasa dinamis punya gradual typing (Python annotations, Ruby Sorbet, JS + JSDoc).
- β "Strong = static." β Python strong + dynamic. C weak + static.
- β "Types membuat kode lebih cepat." β Runtime perf datang dari optimisasi (JIT inline cache, monomorphization), tidak otomatis dari anotasi. V8 bisa mengalahkan Java di beberapa workload; sebaliknya type instability bikin deopt.
- β "Type coverage 100% = bebas bug." β 85% bug JS di studi Gao bukan type-detectable. Type menutup satu kelas, bukan semuanya.
- β "Generics lebih banyak = lebih baik." β Generic gymnastics (HKT emulation di TS,
inferberlapis) menurunkan readability tanpa payoff proporsional. Kalau butuh 40 baris tipe untuk satu fungsi, tipe itu sendiri jadi bug source. - β "Migrasi harus big-bang rewrite." β Selalu inkremental;
allowJs+checkJs+ strict flag bertahap per folder.
β Cek Pemahaman
-
TS menolak
let x: string = nulldenganstrictNullChecks, tapi menerima(obj as any).fielddan menerima method param bivariance. Mana dari keduanya yang melanggar soundness, dan mengapastrictNullCheckstidak membuat TS sound? -
Kamu punya service Go (typed) yang memanggil service Python (untyped, tanpa stub). Di titik mana type-safety hilang, dan kontrol apa yang harus dipasang agar boundary itu aman?
-
Tim mau adopsi TS pada codebase JS 80k LOC. Urutkan langkah migrasi dan jelaskan mengapa
strict: truesejak commit pertama adalah pilihan buruk, sementaraanysebagai strategi permanen juga buruk.
π Poin Kunci
- Static/dynamic = waktu cek; strong/weak = coercion; soundness = jaminan formal β tiga hal berbeda, jangan dicampur.
- Nilai type system bersifat statistik + lokal: menangkap subset bug lebih awal, bukan menjamin kebenaran.
- Erasure berarti semua data dari luar (HTTP, DB, queue, env) tetap butuh validasi runtime. Parse di boundary, tipe di dalam.
- Unsoundness punya titik masuk spesifik:
any,as, raw type, covariance,null,unsafe. Audit titik itu, bukan seluruh kode. - Trade-off asli: anotasi + compile time + rigiditas vs test suite besar + bug produksi + refactor berisiko.
- Desain tipe > banyak tipe. Sum type + exhaustive check mengubah kelas bug jadi compile error.
- Migrasi selalu inkremental, dengan ratchet (strict bertambah,
anymelarang) β bukan big-bang, bukan status quo permanen.
β‘οΈ Eksplorasi Lanjut
- Refinement types (Liquid Haskell) & dependent types (Idris 2, Lean 4) β encode invariant
{v: Int | v > 0}di tipe. Frontier; biaya proof burden tinggi. - Linear/affine types β Rust ownership: memory safety tanpa GC. Bandingkan dengan GC (throughput vs latency vs determinisme).
- Effect systems β Koka, OCaml 5 effects: track side effect di tipe, bukan di dokumentasi.
- Gradual typing di Python β mypy
strict, pyright,beartypeuntuk runtime enforcement; bandingkan overhead. - Schema-first stack β protobuf/Buf, OpenAPI β codegen,
sqlc/Prisma: hilangkan interface manual sepenuhnya. - Bacaan inti: Pierce Types and Programming Languages (ch. 1β9 untuk soundness), Cardelli "Type Systems" (ACM CSUR 1996), Gao et al. 2017, Takikawa et al. 2016.