Type-Driven Development

February 10, 2026

Type-driven development is what happens when you stop treating types as decoration and start treating them as the first draft of your design. You write the contract first, then you write code that is forced to live within it.

The point of types (PLT, lightly)

A type system is a way to say something non-trivial about a program without running it. Not "the program is correct," but "this kind of wrong thing can't happen here anymore."

At its core, a type system is a proof obligation. Every time you write x: int, you're claiming "this value will always be an integer at this point." The type checker's job is to verify that claim statically—before any bytes move. This is what PLT people call soundness: if the checker says yes, the bad thing doesn't happen at runtime. The tradeoff is completeness—some valid programs get rejected because the checker can't prove they're safe. Every type system lives somewhere on that line.

We're keeping this practical: simple types in the style of TypeScript, Rust, or Haskell. There's a whole deeper world—dependent types, linear types, refinement types—that makes the theory richer and the trade-offs more nuanced. Consider this an introduction, not the complete picture. (Also: I'm not a type theorist. Don't know nearly enough about actual type theory or PLT to claim expertise here. Just sharing what I've picked up along the way.)

Dynamic languages aren't untyped. They're uni-typed. Every value inhabits one big type, and classification happens at runtime—which means errors happen at runtime too. Adding type hints (Python, TypeScript) is choosing to move some of those checks earlier. You give up nothing. You just learn about the fire before the building is burning.

Type-driven development uses that leverage early. Before you have a handler. Before you have a database table. Before you have three versions of the same payload drifting through Slack.

You don't start with implementation. You start with the shape of reality.

Local contracts, not vibes

In dynamic languages, the default contract is: "trust me." It works until the day you refactor a function and nothing breaks loudly—things just break later, elsewhere, under load.

There is a reason every mature dynamic ecosystem eventually grows a type layer. Python got typing. JavaScript became TypeScript. Ruby got Sorbet, PHP got Hack. The pattern is always the same: the codebase gets large enough that the person writing a function and the person calling it are no longer the same person—or the same person six months later, which is effectively a stranger.

Types formalize the interface. They turn "I think this returns a dict with a user_id key" into "this returns UserView, which has a user_id field of type UserId." The negotiation becomes searchable, refactorable, diffable. Grep works. Your editor works. Your future self works.

Data-driven development is the bigger story

If your app "deals with data," your app is a pipeline. Requests come in. Data changes form. Responses go out. Bugs are usually shape bugs, not algorithm bugs.

A user submits a form. The handler parses it into a struct. The struct gets validated. The validated data gets written to a table. A response gets built from the written row. Every one of those arrows is a place where the shape of the data can silently change—a field gets dropped, a null sneaks in, an enum gains a variant no one handles. The entire request lifecycle is a chain of transformations, and each transformation is a place where a type mismatch can hide.

This is why contract-first thinking keeps resurfacing as "API-first": define the contract early, get feedback before code hardens, and use it to align teams. The contract becomes the shared source of truth—useful for early review, for mocking, for generating parts of the ecosystem around the API.

Type-driven development is the same move, but one level down:

Data-driven development: the schema is the product.

Type-driven development: the schema has a spine inside your code.

A request/response sketch (with error variants)

Here's the whole idea in one small, boring contract.

You don't begin with the handler. You begin with the shapes:

// Rust-flavoured pseudocode (idea > syntax)

struct CreateUserReq {
    email: String,
    age: Option<u8>,
}

struct UserView {
    id: UserId,
    email: String,
}

enum CreateUserErr {
    InvalidEmail,
    DuplicateEmail,
    RateLimited,
    StorageDown,
}

fn create_user(req: CreateUserReq) -> Result<UserView, CreateUserErr> {
    // implementation later
}

That's type-driven development in practice:

  • Request shape is explicit.
  • Response shape is explicit.
  • Errors are explicit variants, not "raise something and hope a middleware catches it."

Now the rest of your architecture has somewhere to stand. Your HTTP layer becomes an adapter: decode → call → map error variant → encode. Your database layer becomes another adapter: persist → return either data or a specific failure.

The same idea works in Go with structs and explicit error values. In Python with Pydantic models and typed exceptions. The syntax changes. The core move doesn't: make the boundary explicit and force callers to handle the unhappy paths intentionally.

AI codegen changed the economics

AI assistants write code fast. They also write plausible code fast, which is more dangerous than obviously broken code.

The pattern is well-documented: generated code can include bugs and security vulnerabilities, with no guarantees of correctness. This matches the lived experience: the assistant guesses the missing pieces, and the guesses often fail at the boundaries—wrong payload shape, wrong return type, missing error handling, silent None paths.

Type-driven development is one of the cheapest mitigations:

  1. Give the assistant the contract (signatures, types, error variants).
  2. Let it fill in implementation inside that fence.
  3. Let a type checker be the first reviewer.

Tooling is starting to acknowledge this explicitly: Astral's ty was designed with diagnostics meant for both humans and agents in mind.

Types won't make AI-generated code correct. But they make it harder for incorrect code to fit.

The practical takeaway

If your system is made of data transformations—and most backend systems are—the only sane design is contract-first. Put the contracts where they can't be ignored: request/response types, function signatures, explicit error variants.

Write those first. Then write everything else like you're filling in a form you already understand.

If the most dynamic language community on earth (JavaScript?) voluntarily adopted a type system to stay sane, maybe we all should start doing it.