Type Systems

Also known as: static typing, type checking, type contracts, strongly typed programming

TL;DR

A type system is the contract that says 'this variable holds an integer, that function returns a User.' Static type systems (Rust, TypeScript, mypy) catch the contract at compile-time.

A type system answers one question: what shape of data is this variable / function / interface allowed to hold? The answer determines what bugs the compiler catches versus what bugs ship to production, what your IDE can autocomplete, and how much of a “this looks like JSON, maybe it parses correctly” your codebase tolerates.

Three families matter in practice:

  • Static — checked at compile time, zero runtime cost. Rust, TypeScript, Java, Go, mypy/Pyright on Python. The compiler refuses to ship code that mis-uses a type.
  • Dynamic — checked at runtime, type errors surface as exceptions. Vanilla Python, Ruby, JavaScript without TypeScript. Flexible, fast to prototype, accumulates bugs at scale.
  • Runtime / structural validation — explicit schema checks on data crossing a boundary. , Zod, JSON Schema, Protobuf at the wire. Static typing for data the compiler can’t see.

The boundaries-vs-inside framing

Static types are for code; runtime types are for data crossing into your code. You need both, applied at the right layer.

The shape of a real production system:

  • Inside your process: static types are sufficient. The compiler proves the function signatures match the callers. No need to re-check at runtime; the bytes that travel between Python functions are already typed.
  • At the edges (HTTP, files, LLMs, queue, DB): runtime validation. The bytes that arrive over the wire are untyped until you parse them. Static types are aspirational here — the compiler can’t tell you whether the JSON you’re parsing matches your declared shape. Pydantic / Zod / JSON Schema fill that gap.

Get this wrong in either direction and you get a familiar pathology:

  • No static typing, only runtime checks. Every function re-validates its inputs because nothing upstream proved them. You pay validation cost on every internal call, and you discover function-signature drift through production errors.
  • No runtime checks, only static typing. Your code claims data: User, but data arrived as JSON from an untrusted source. The first time the JSON differs from User’s shape, you get an AttributeError deep in business logic instead of a clean “this request is malformed” at the boundary.

The LLM era widened the boundary

When a model claims to return {action: 'sell', amount: 100}, what arrives is a string that parses to that shape most of the time. Production systems need:

  1. Schema declared up front. A Pydantic model, a Zod schema, or a JSON Schema describing the expected shape.
  2. Constrained decoding at the inference layer to make the model’s output adhere to the schema. (See .)
  3. Runtime validation of the output anyway, because constrained decoding is best-effort, not guaranteed.

This is the same boundary pattern as HTTP requests — except the wider variety of failure modes makes type-checking the only thing standing between “the LLM said something unexpected” and “production crashed.”

Why static + runtime is the answer

The world tried both for two decades:

  • Pure dynamic (Python, JS): productive at small scale, hostile at large scale. Maintenance cost compounds. Refactors become archaeology.
  • Pure static, runtime-naive (early Go, early Rust): the moment your code touches the network, you’re back to manually parsing bytes into trusted shapes. The compiler can’t help you across process boundaries.

The mature shape is static everywhere it’s cheap (your own code), runtime validation at every boundary (HTTP, LLM, file, DB, message queue). The libraries that succeed (Pydantic, Zod, Protobuf) make boundary validation feel as native as static typing. The languages that succeed (Rust, TypeScript, mypy-clean Python) make static typing feel as ergonomic as dynamic.

Concrete defaults

  • Python: mypy/Pyright for static; Pydantic for runtime. The strictest mypy config you can tolerate.
  • TypeScript: strict mode on for static; Zod or io-ts for runtime. unknown over any everywhere.
  • Rust / Go: static is built in. Use serde/encoding-aware libraries at the wire; the static type system handles the rest.
  • For LLM outputs specifically: declare the schema in code (Pydantic / Zod / JSON Schema), pass it to the LLM provider’s structured-output API, then re-validate on receipt. Three layers of defense, all cheap.

Production code is only as type-safe as its weakest boundary. Find the boundary; type the boundary; everything inside is then the compiler’s job.

Go further

Static vs dynamic — is the war really over?

Yes. Every large Python codebase added type hints. JavaScript ecosystems have migrated en masse to TypeScript. Ruby and Elixir picked up gradual typing. The dynamic camp didn't lose because dynamic was wrong — it lost because the cost of guessing what result contains at line 4000 of a project is greater than the cost of writing result: dict[str, list[User]] once. The remaining honest argument is gradual vs full static, not dynamic vs static.

Where exactly does runtime validation pay back?

At every boundary where data enters your process from outside: HTTP requests, queue messages, file reads, LLM responses, database results, CLI arguments. Inside your process, static types are cheaper (they cost zero at runtime); outside your process, types are aspirational unless you re-check. Pydantic and Zod live at those boundaries — convert untyped bytes into typed objects at the door, then trust the static type checker the rest of the way.

What changed when LLMs entered the picture?

LLM outputs became another untrusted boundary. A model claiming to return {action: 'sell', amount: 100} might return {action: 'sell', amount: '100'}, or {Action: 'sell'}, or freeform prose. Function calling and structured-output APIs use schemas (JSON Schema, Pydantic models, Zod) to constrain the model AND validate the output. The same type system that powers your HTTP layer now powers your LLM layer; the boundary just got wider.

ZeroEntropy
The best AI teams build with ZeroEntropy models
Follow us on
GitHubTwitterSlackLinkedInDiscord