Part 5 – Indexed Types
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:
- 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




When we create Objects, all have the same thing in common: “property name and value”.
person = { first: 'Jesse', powerLevel: 9001 }Code language: JavaScript (javascript)
We access that person’s first property in 2 ways, either person.first, or person['first']. At runtime, accessing the first property will give us the first’s value, 'Jesse'.
Product types are usually a collection of types grouped together:
type Person = { first: string, powerLevel: 9001 }
We know how to access object values at runtime, but how do we access type property names?
We access them the same way we do objects using the name index method (e.g. person['first']), except it’s “type name and type value”:
type First = Person['first']Code language: JavaScript (javascript)
So type First is also just string. Sadly, u can’t do First.first, but if you remember “index” similar to how you access an Array’s items, like myList[2], basically the brackets [], that helps me remember.
A Money type may look like this:
type Money<C> = { currency: C, amount: number }Code language: HTML, XML (xml)
The reason you care about indexing is for some operations you want to keep safe, you need to know specific parts of the type. For Money that’d be Money['currency'].
With money, you don’t really care a/b the amounts when adding, you first ask “Are currencies the same type?” b/c you can’t just add USD (United States Dollars) + EUR (Euro)’s together. Each currency is worth a completely different amount. Converting them to & from each other, u have to know the exchange rate. It changes every second so we’ll assume u got it from somewhere.
This business rule, or “invariant”, is often baked into money libraries, like dinero.js. However, what a lot of those libraries have in common is they do not enforce those rules in the type system; instead, they go halfway then explode at runtime.
function add(a:Money, b:Money):Money {
if(a.currency !== b.currency) {
throw ErrorCode language: JavaScript (javascript)
Code like this assumes you’ll create the same types & ensure you don’t call add w/USD + EUR. That is not encoding the business logic in the type system. Instead, you’d do something like:
function add<C>(a:Money<C>, b:Money<C>):Money<C> {Code language: HTML, XML (xml)
Notice there’s only one C, or currency type parameter there, not two; so that means u HAVE to pass the same currency to add, USD or EUR. There are additional nuances around TypeScript & other languages inferring C is a Union so it could allow USD; use the NoInfer type for the 2nd parameter, but u get the point: types enforce the business rule. Indexing gives you ability query & refine the types when u’re working w/2 Product Types + u need to know details abt how to combine them. Math w/Money, math w/ temperatures & measurement (Metric vs Imperial), or account migration & other mapping operations that need to be type safe.
Leave a Reply