Type Driven Development: Make Impossible States Impossible

Written by

in

Part 24 – Make Impossible States Impossible

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

Without types, you typically model behavior with tests to both validate that behavior, & ensure you have a shared understanding of it. Those class methods or groups of functions ensure the domain code doing rules + giving you valid data as well as the more side-effecty code of interacting w/the outside world works the way you expect.

Types, if you’re into algebra, can greatly speed up, & most of the time simplify what you write, + reduce how much you have to write. This includes reducing how many tests you need to write. The types can both narrow, or shrink, how many happy/unhappy/edge cases you need to deal with, typically in tests. This has a nice maintenance cost too; if you need to to change things, you can utilize the compiler as a way to ensure existing situations you want to happen still do and just extend or modify for the new one.

We’ve shown how Branded Types prevent you mixing up values, Products ensure data that belongs together changes together, and Unions help round out your 3 most powerful ways to ensure bad situations just cannot happen in your code. Using these to model your problems and ensure only problems you _expect_ to happen actually can is a wonderfully fast way to learn, iterate, and make great software.

For LLM’s, they can both understand your types, infer what to build much like unit tests, and help ensure those invariants (business rules) are not violated, adding a 2nd type of compiler while you work and change your code as you learn. On the flip-side, they’re flexible; many languages have a spectrum of gradual even if they’re not marketed as such, and so the LLM’s are great at giving you options of both _how_ type-safe you want something, as well as giving you options on how to approach it.

Types provide a fast to create, iterate, and long-term readable way to encode how your software works, for both you and AI, removes a lot of fear from changing the code, and allows you to make impossible situations less likely to occur. To be clear, you still need tests, but types can make working with TDD and tests both much easier to enforce things, make your tests more focused, as well as ensuring they’re all, tests and code, much safer to change.

Keep in mind type narrowing, building your domain, learning is all an interative process, and there is often a ROI balance of “how much type-safety is actually valuable here” as well as “are these types readable in this context / on this team”. Making Impossible States Impossible should definitely be the goal, but recognize it’s not black and white; it’s often a spectrum with nuance. Some type systems are just terrible and require more tests. Some type systems have a steep learning curve.

Comments

Leave a Reply

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