Part 25 – Type Driven Development
This is a series of posts I’m writing about using types as another tool in software development, Continuous Delivery, & keeping LLM’s honest. They’re also a design & refactoring tool, a communication tool, and reduce how many tests you have to write.
Update: Wed, Aug 12, 2026 – updated both examples to compile if you copy paste them.
Parts
- Part 1 – Branded Types
- Part 2 – Product Types
- Part 3 – Union & Discriminated Unions
- Part 4 – Non-Empty Collections
- Part 5 – Indexed Types
- Part 6 – unknown vs any
- Part 7 – Result
- Part 8 – Schema
- Part 9 – Total Function
- Part 10 – Errors as Values
- Part 11 – Property Tests
- Part 12 – Type Proofs
- Part 13 – Exhaustiveness Checking
- Part 14 – Parse, Don’t Validate
- Part 15 – Anti-Corruption Layer
- Part 16 – Opaque Types
- Part 17 – Maybe
- Part 18 – Smart Constructors
- Part 19 – Pipeline
- Part 20 – Railway Oriented Programming
- Part 21 – Typestate
- Part 22 – Capabilities
- Part 23 – Immutability
- Part 24 – Making Impossible States Impossible
- Part 25 – Type Driven Development: How to do it
- Part 26 – Final Thoughts




Type Driven Development (TyDD) is a lot like Test Driven Development (TDD); a design technique that results in proofs for easy to change code later. In most languages, you’ll use together; more types in a better type system, more tests in a bad one. Here’s 4 ways to approach it.
We’ll be enforcing Secondary Accounts can only cancel their own Transactions, Primary Accounts can cancel any Transaction. I’m Primary, & my daughter, Sydney, is Secondary.
In TDD:
1. write our expectation
expect(
canCanel(
{ accountType: 'primary', id: 'jesse' },
{ transactionID: '4', accountID: 'jesse' }
)
).toBe(true)Code language: CSS (css)
2. implement the function in simplest way to make it pass
3. refactor or move into next test
1st: In good type systems, you just write code, fix the red, refactor. Each change that breaks the code, you let the compiler tell you what to fix. You keep the changes small.
2nd: The @ts-expect-error comment directive way allows you to write types and functions normally, but for unhappy paths you must use the comment above them:
1. create a failing type function
// fixtures
type UserID = string & { brand: 'UserID' }
type ProductID = string & { brand: 'ProductID' }
declare const userID: UserID
declare const productID: ProductID
// function
declare function getUser(id: string): void
// failing test; should not compile
getUser(productId)
Code language: PHP (php)
2. simplest function to make it pass
// declare function getUser(id: string): void
// fixed function
declare function getUser(id: UserID): voidCode language: PHP (php)
3. Now your @ts-expect-error comment will no longer be underlined red. Refactor types OR do happy paths without the comment (e.g. cancelPayment(jesseTransaction, jesse) )
Caveat: The comment must be above compiler error; if you multi-line your types like me, you may have to move it.
3rd: The type proof way is similiar:
1. write a failing proof
// fixtures
// same as above
// proof functions
type Assert<_Condition extends true> = void
type isAssignable<From, To> =
[From] extends [To] ? true : false
type Not<Condition extends boolean> =
Condition extends true ? false : true
// Unhappy path, failing proof
type _ProductIdCannotBePassedToGetUser = Assert<
Not<
IsAssignable<
ProductID,
Parameters<typeof getUser>[0]
>
>
>
Code language: JavaScript (javascript)
2. make proof pass by improving the type
// old function
// declare function getUser(id: string): void
// new function
declare function getUser(id: UserID): voidCode language: PHP (php)
3. either refactor or move to additional proof tests
The fourth is just to intermingle 1 of the above 3 _while_ you do TDD. That’s what I do.
Part 26 will have final conclusions.
Leave a Reply