Skip to content

Why I’ve Started Using Branded Types in TypeScript

I’ve had those moments where everything runs perfectly on my machine, only to have things fall apart the second they hit production.

2 min read
Branded Types
Nominal Typing
TypeScript
IDs
Design Tokens
Frontend

I’ve had those moments where everything runs perfectly on my machine, only to have things fall apart the second they hit production. Usually, it’s not some massive logic error, but something much more trivial: I accidentally passed an OrderId into a function that was supposed to receive a UserId. On paper, they’re both just strings, so TypeScript didn't blink.

That was my wake-up call regarding the gaps in TypeScript’s structural typing. As long as the shape is the same, TypeScript treats them as identical. A string is just a string. When you're juggling different IDs or tokens, that's a risky way to live.

To fix this, I started using branded types.

The concept is simple. I attach a "brand" or a unique tag to the type. At runtime, the value is still just a regular string, but while I'm writing code, TypeScript treats them as two completely different entities. It’s my way of getting some of that nominal typing security without leaving the TypeScript ecosystem.

That said, I’ve realized you shouldn't brand everything. If I started branding every number representing pixels or milliseconds, the code would just be filled with unnecessary ceremony. Adding types for the sake of it just makes the codebase feel heavy.

It makes more sense to reserve branded types for the crucial stuff: IDs passing through different packages, sensitive API keys, or tokens that show up in URLs and cache keys.

I’ve also learned that you need discipline with how you handle constructors. I don't want these types to be based on "hope." I usually wrap them in a specific function that validates the value upfront—checking the format, for example—before casting it to the branded type. If the validation fails at the "entry point" (like when grabbing data from a URL), the problem is caught right there. The rest of the code downstream doesn't have to worry about bad formats.

Of course, this approach has its downsides. If you create too many constructors that perform heavy logic, you accidentally turn your type helpers into actual services. It’s all about finding a balance.

Ultimately, branded types are a lifesaver when you're working in shared packages used by multiple apps. But if it's just for a local variable inside a single function, it's probably overkill.

More another time.