← semua materi
05:00 Arsitektur Β· 08 Nov 2026 Β· 05:02

πŸ“š 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:

  1. Memilih bahasa/tipe strategi per komponen (bukan per proyek): di mana type checker memberi leverage, di mana ia hanya menambah gesekan.
  2. Menilai klaim "types prevent bugs" secara kuantitatif, bukan slogan.
  3. Tahu persis lubang di type system yang dipakai (TS any, as, erasure, bivariance) dan menutupnya dengan boundary validation β€” bukan dengan harapan.
  4. 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

  1. as sebagai kebohongan. const user = await res.json() as User β€” compile bersih, user.address.city β†’ TypeError: Cannot read properties of undefined di produksi. TS tidak memvalidasi runtime; as hanya mematikan compiler. Fix: userSchema.parse(await res.json()).

  2. 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.

  3. Generics erasure menelan bug. Java: List<Order> di-cast ke List lalu isi String; error muncul jauh dari sumber. Fix: jangan raw type, gunakan Class<T> token untuk validasi runtime bila perlu.

  4. Type-unsoundness tersembunyi di library. Java array covariance, TS method bivariance (callback (e: MouseEvent) => void lolos sebagai (e: Event) => void). Bug hanya muncul saat event yang salah dikirim.

  5. any menyebar seperti wabah. Satu any di boundary β†’ semua konsumennya kehilangan tipe. ESLint no-explicit-any, no-unsafe-* di typescript-eslint jadi kontrol wajib.

  6. 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.

  7. Schema drift DB ↔ ORM. Kolom nullable di 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


βœ… Cek Pemahaman

  1. TS menolak let x: string = null dengan strictNullChecks, tapi menerima (obj as any).field dan menerima method param bivariance. Mana dari keduanya yang melanggar soundness, dan mengapa strictNullChecks tidak membuat TS sound?

  2. 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?

  3. Tim mau adopsi TS pada codebase JS 80k LOC. Urutkan langkah migrasi dan jelaskan mengapa strict: true sejak commit pertama adalah pilihan buruk, sementara any sebagai strategi permanen juga buruk.


πŸ”‘ Poin Kunci


➑️ Eksplorasi Lanjut