Type Driven Development: Schema

Written by

in

Part 8 – Schema

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.

Parts
  1. Part 1 – Branded Types
  2. Part 2 – Product Types
  3. Part 3 – Union & Discriminated Unions
  4. Part 4 – Non-Empty Collections
  5. Part 5 – Indexed Types
  6. Part 6 – unknown vs any
  7. Part 7 – Result
  8. Part 8 – Schema
  9. Part 9 – Total Function
  10. Part 10 – Errors as Values
  11. Part 11 – Property Tests
  12. Part 12 – Type Proofs
  13. Part 13 – Exhaustiveness Checking
  14. Part 14 – Parse, Don’t Validate
  15. Part 15 – Anti-Corruption Layer
  16. Part 16 – Opaque Types
  17. Part 17 – Maybe
  18. Part 18 – Smart Constructors
  19. Part 19 – Pipeline
  20. Part 20 – Railway Oriented Programming
  21. Part 21 – Typestate
  22. Part 22 – Capabilities
  23. Part 23 – Immutability
  24. Part 24 – Making Impossible States Impossible
  25. Part 25 – Type Driven Development: How to do it
  26. Part 26 – Final Thoughts

To do anything of value in software, we have to get unsafe data from outside our type system into it. We saw how unknown forces us to type narrow, but type narrowing is dangerous, tedious, and it’s hard to be thorough.

cow = JSON.parse(json)
if(cow 
  && 'age' in cow 
  && typeof cow?.age === 'number')
  // could be NaNCode language: JavaScript (javascript)

Then comes the architecture problem; how do you DRY (Don’t Repeat Yourself) this? Multiple parts of your code have:

type Cow = { age: number }

But later the back-end changes age to be optional:

type Cow = { age?: number }

So you have to update both your types and parsing code. This occurs a lot, which then creates Shotgun Parsing all throughout your code:

if(cow.age) {
  // use age
} else {
  // throw an error
}Code language: JavaScript (javascript)

This is because despite types, no one can trust the data. No code knows where the data came from; “Maybe age hasn’t been validated yet? Better validate just in case.” You started coupling the types through your code in hopes everyone would know “what a Cow is”, but now all the code is repeatedly validating the data. Worse, only parts are validated:

if(cow.name) {
// if no name, throws an error
// what about age? Who knowsCode language: JavaScript (javascript)

Even code formatting, e.g. Oxcfmt/Prettier/Biome, is bypassed b/c data is at runtime, so Python snake case data:

{ first_name: 'Moo', last_name: 'Cow' }Code language: CSS (css)

Can start infecting your camelCase style code. Since validation is often impromptu, it’s not all the same or thorough. Empty strings ”, NaN for numbers, and optional properties on data cause runtime errors or null checks like if(thing?.and?.some?.prop) adding lots of validation code repeatedly.

Lastly, the data you get isn’t always what you want. REST API’s can send back 100 JSON values, but you only need 2, forcing you to write & maintain a lot of parsing code you’ll never use. JSON can have optional values, but you require that data, knowingly parsing the thing you don’t like.

Schema’s solve all this. 1 source of truth, it either parses or it doesn’t, the type is validated data all of your code can trust, use without modifying, and not have to validate. The shape can be what you want/need instead of coupling to what the back-end gives you, then having to update all your code when the back-end changes. Zod/ArkType/Effect all provide schemas:

// define your parsing functions
cowSchema = z.object({
  // a number that is not NaN and positive
  age: z.number().gte(0),
  // a string name instead of Python's snake case first_name
  name: z.string().transform( d => ({ …d, name: d.first_name }) )
})

// use it to create your type
type Cow = z.infer<typeof cowSchema>

// parse your data
json = JSON.parse(data)
cow = cowSchema.parse(json)
// cow is a Cow type and safe to use!Code language: JavaScript (javascript)

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *