Part 1 – Branded Types
This is a series of posts I’m writing about using types as another tool in software development. Automated Tests & Builds are part of Continuous Delivery. As the use of LLM’s increases the amount of code, many of us are going full shields double-front to double down on best practices to handle the increased influx of code. Types aren’t often talked about as a way to help with your shift left process; e.g. speeding up automated quality checks locally. They’re also a design & refactoring tool, a communication tool, and reduce how many tests you have to write.


Let’s talk about the 2 most basic problem using types: Naming Things & Primitive Obsession.
Naming comes from Domain Driven Design. The words we use for things should be the same & reflected in the code. If the User, Product Owner, + Business Analyst all call it a “Customer Account” then we should too as should the code.
This prevents developers from using the wrong word like just “User”, then later making some bad assumptions, such as adding incorrect logic in code simply because the “meaning” of User is different than a Customer Account. e.g. Allowing “Users” to purchase orders when only “CustomerAccounts” should be allowed to do so. Clear communication is huge.
2nd, you can help the compiler know these differences. Often developers will default to primitives, e.g. a string to differentiate between a User and a Customer Account. Here is an incorrect parsing of headers:
const customerID:string = request.headers['x-user-id']
const userID:string = request.headers['x-customer-id']Code language: JavaScript (javascript)
The problem is the compiler cannot tell the difference between primitives. That means this code looks ok:
purchase(customerID)
But in fact, you’re allowing a user to purchase something when what you meant to have happen was the customer should purchase it. Even good type systems like Python, TypeScript, and even Elm and Scala will allow this to happen if the purchase function is typed like:
type Purchase = (customerID:string) => ...Code language: JavaScript (javascript)
Instead, use Branded types (or Wrapper Types) to help the compiler act in a Nominal way. Nominal meaning it knows that a CustomerID and UserID are different.
First, define the 2 types:
type UserID = string & { brand: 'UserID' }
type CustomerID = string & { brand: 'CustomerID' }Code language: JavaScript (javascript)
Then update your function to use it:
type Purchase = (customerID: CustomerID) => ...Code language: JavaScript (javascript)
Now the compiler won’t let you mix up the ID’s, nor do you need to write unhappy path tests when you pass in the wrong ID.
When parsing from unknown sources, like request headers or JSON, schema libraries like Zod support this. Instead of:
const Headers = z.object({
'x-user-id': z.string(),
'x-customer-id': z.string()
})Code language: JavaScript (javascript)
Instead go:
const Headers = z.object({
'x-user-id': z.string().brand<'UserID'>(),
'x-customer-id': z.string().brand<'CustomerID'>()
})Code language: JavaScript (javascript)
The compiler can help you distinguish between 2 things; make “thing” the same word your Users, Design, and Business use.
Leave a Reply