Case file — T1

Domain-Driven Types

If a hedgehog can fly, your types are wrong.

The type system is the cheapest documentation a codebase will ever carry. Encode the domain in types so the invalid states cannot compile, and half the comments retire themselves before the next reader arrives.

ByAdam LewisPublished29 Apr 2026Reading8 minVersionv1.0ConfidenceHigh
§0b

Opinion

I write the type before the function, every time. Most of the comments I have read in production code are explanations of constraints the type could have carried for free: this string is a UUID, not just any string; this number is in pence, not pounds; this can be null only on Tuesdays. The fix is the type. Lift the constraint into the signature and the prose dies on the spot. See F5 Self-Documenting Code: the comment is the receipt for a type that was not written.
§0c

AI eyes only

Rule: types encode the rules of the domain. Make invalid states unrepresentable.

Reject: string for ids when a brand exists. Reject: optional fields for states that should be discriminated unions. Reject: any to “get past the compiler”.

Generate: branded primitives for ids and value objects. Discriminated unions for states. Validated DTOs at boundaries. Result<T, E> for fallible operations.

Diagnostic: try to construct an invalid instance using only the type. If you can, the type is too loose; tighten before continuing.

§0d

Why?

The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

Origins

The case for types-as-documentation predates TypeScript by decades. Tony Hoare's 1965 invention of the null reference, the “billion-dollar mistake” he later named, is the negative case: a type that admitted the bad state shipped the bad state every time. Edwin Brady's Type-Driven Development with Idris (2017) is the positive case in book form: write the type first, let the type drive the test, let the test drive the implementation. 2Edwin BradyType-Driven Development with Idris (Manning, 2017). Book-length treatment of type-first design, with worked examples in Idris.

Yaron Minsky's 2010 Jane Street talk, Effective ML, gave the principle the name that stuck: “make illegal states unrepresentable.” 1Yaron Minsky“Effective ML / Make Illegal States Unrepresentable” (Jane Street, 2011). The canonical name for the discipline; the type system as a positive constraint, not a passive checker. Scott Wlaschin's Designing with Types series translated the discipline from ML into F# and made it accessible to a working developer audience. 3Scott Wlaschin“Designing with Types” series (fsharpforfunandprofit.com, 2013). The accessible translation of the ML discipline into F#, with practical worked examples. Eric Evans's Domain-Driven Design (2003) is the architectural complement: bounded contexts, value objects, ubiquitous language. The model is what types encode in the first place. 4Eric EvansDomain-Driven Design (Addison-Wesley, 2003). The architectural complement; the model that types encode.

§2

Quotes

Make illegal states unrepresentable.

Yaron Minsky · Effective ML, Jane Street (2011)

The type system is your friend. It's not just there to catch typos — it's a design tool. A well-designed type system can make whole classes of errors impossible.

Scott Wlaschin · Designing with Types (2013)

Define the type, then define the function. The type is a specification; the function is a proof.

Edwin Brady · Type-Driven Development with Idris (2017)
§3

Evidence

Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.

  1. 01
    Yaron Minsky · 2011
    Coined the canonical phrase. The type system as a constraint-checker, not just a linter.
  2. 02
    Scott Wlaschin · 2013
    Translated the ML/Haskell discipline into F# with worked examples — single states, sum types, branded primitives, validation by type.
  3. 03
    Edwin Brady · 2017
    Book-length treatment of types as the primary design surface. Define the type, then define the function — the type is the spec.
  4. 04
    Eric Evans · 2003
    Architectural companion: value objects, bounded contexts, ubiquitous language — the domain model that the types encode.
  5. 05
    Kevlin Henney · 2010
    Names and types are the same problem from two angles — both let the code carry meaning the prose was patching.

Eight sources, one direction. The honest counter is that types over-engineer simple code; the reply addresses it directly below.

§4

Examples

Viewing: TypeScript.
Avoid
Filehedgehog-inventory.ts
// Before: three primitives. Pass a region where the id was meant; nothing flags it.function tagHedgehog(id: string, weight: number, region: string): void {  writeTag({ id, weight, region });}// caller — id and region transposed; compiler is silent:tagHedgehog("ashdown-forest", 412, "HH-0042");
Prefer
Filehedgehog-inventory.ts
// After: domain types. The illegal call cannot compile.type HedgehogId = string & { readonly __brand: "HedgehogId" };type WeightGrams = number & { readonly __brand: "WeightGrams" };type ForestRegion = "ashdown" | "epping" | "sherwood";function tagHedgehog(id: HedgehogId, weight: WeightGrams, region: ForestRegion): void {  writeTag({ id, weight, region });}// caller — transposed call now fails to compile:tagHedgehog("ashdown", 412, "HH-0042"); // type error: string is not HedgehogId
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in tsconfig.json. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
stricttscthe umbrella flag — turns on noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, alwaysStrict and useUnknownInCatchVariables.
noUncheckedIndexedAccesstscarray[i] returning T instead of T | undefined — the single biggest hole in 'strict' that ships bugs.
exactOptionalPropertyTypestscthe silent equivalence between { x?: number } and { x?: number | undefined }.
@typescript-eslint/no-explicit-anytypescript-eslintany type written by hand — the only legal escape is unknown plus a type guard.
@typescript-eslint/explicit-function-return-typetypescript-eslintmissing return-type annotations on exported functions.
@typescript-eslint/no-unsafe-assignmenttypescript-eslintany leaking into typed code through untyped JSON, library boundaries.
tsconfig.jsonconfiguration snippet
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "useUnknownInCatchVariables": true
  }
}
§4c

AI rules

File.cursor/rules/t1-domain-driven-types.mdc
---
description: Prickles T1 — Domain-Driven Types
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---

## Prickles T1 — Domain-Driven Types

> Canon: https://prickles.org/tenet/domain-driven-types/T1

Brand the primitives. A user ID is not a string; an email is not a string; a UUID is not a string. Wrap each domain primitive in a typed brand the compiler enforces, so passing a BookingId where a UserId is expected fails at compile time.

If two values of the same primitive type can never be assigned to each other in the domain, give them different types. Internal IDs and external Stripe IDs are not interchangeable; cents and pounds are not the same number; ISO-8601 strings and unix timestamps are not the same date.

Model invalid states out of existence. A discriminated union with one tag per legal state beats a flag-soup of optional booleans. If a hedgehog can be both isHibernating: true and isFeeding: true, the type is wrong.

Validators sit at boundaries, not throughout the codebase. Parse once at the edge — JSON.parse, route input, external API response — then trust the type for the rest of the call. Parse, do not validate.

When a function takes more than two parameters of the same primitive type, introduce domain types or a parameter object. transfer(from: string, to: string, amount: number) is a bug waiting to happen; transfer(from: AccountId, to: AccountId, amount: Money) removes the entire class.

Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.

§5

Counter-argument

Counter

The honest steelman is over-engineering. A small script does not need a Result<T, E> for a one-line transformation; the friction of writing the type costs more than the bug it prevents at that scale. Dynamic-language advocates will go further: types ossify a design before the design is known, and the cost of changing a pervasive type is steeper than the cost of changing a pervasive call. Sandi Metz's The Wrong Abstraction applies here too: the wrong type is harder to remove than the wrong function.

§6

Counter-argument retort

Reply

Granted in scope, not in principle. For a 30-line script, skip the brand. For a system that ships, type the signatures and the comments retire themselves. The cost of changing a pervasive type is large only because the type is doing real work; that work was the documentation that nobody wrote because they trusted the comment instead. Pay the friction at the signature, save the debug at three in the morning. 5Martin FowlerValueObject (martinfowler.com/bliki, 2016). Pattern reference for the value-object idea that branded primitives implement at the type level.

§7

Notes

  1. [1]Yaron Minsky“Effective ML / Make Illegal States Unrepresentable” (Jane Street, 2011). The canonical name for the discipline; the type system as a positive constraint, not a passive checker.
  2. [2]Edwin BradyType-Driven Development with Idris (Manning, 2017). Book-length treatment of type-first design, with worked examples in Idris.
  3. [3]Scott Wlaschin“Designing with Types” series (fsharpforfunandprofit.com, 2013). The accessible translation of the ML discipline into F#, with practical worked examples.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-04-27