type-driven-design
Type-Driven Design
Core rule: parse, don't validate (Alexis King). Validation inspects a value and returns it unchanged — the knowledge that it passed evaporates immediately, so every downstream function must either re-check or blindly trust. Parsing converts less-structured input into a more-structured type that cannot represent the invalid case, once, at the boundary. From then on, possession of the value is compiler-enforced proof of validity everywhere it travels.
Flag:
-
Primitive obsession: domain concepts passed as bare
string/number.userId: stringaccepts an email, an id from the wrong system, or""— all compile. Replace with a branded type whose parse function is the only producer:type UserId = string & { readonly __brand: "UserId" }; function parseUserId(raw: string): UserId { /* check or throw */ }Branding is required in structurally-typed languages — a bare
type UserId = stringalias enforces nothing. (Rust/Haskell: newtype; elsewhere: a small nominal wrapper class.) Keep the brand/constructor private so the parse function can't be bypassed. -
Comments doing a type's job: "Amount in whole cents; no decimals" on
amount: numberis a plea. ACentstype whose parser rejects decimals is a law. Any doc comment stating a constraint on a field or parameter is a type waiting to be written — and once the type exists, the comment is deleted, not kept as decoration. -
Prose links where the compiler could enforce: a comment like "see
REGION_CODESin../constants" is an unenforced pointer that goes stale silently. Derive instead —type Region = (typeof REGION_CODES)[number]— so the constant is the single source of truth and drift is a compile error, not a doc bug.