TypeScript Branded & Type
Explore the concept of branded types in TypeScript to enhance code quality and consistency, and learn how to implement them using TypeScript's type system.
9 juin 2024
Published
Hugo Mufraggi
Author

TypeScript Branded & Type
Today, I’m sharing my latest discovery in the TypeScript ecosystem. This little tip is a real game changer and will increase your code's quality, consistency, and robustness.
You can use it everywhere, both in front-end and back-end development, and with packages like Zod, Effect.Schema, or purely in TypeScript.
Branded Types
TypeScript provides a very powerful typing system, but it has some limitations. One advanced technique to enhance TypeScript’s type system is through branded types.
A branded type is a way to create a unique type by combining a base type (like a string or number) with a unique brand. This brand is usually a symbol or a specific object, which makes the new type distinct from other types, even if they share the same base type. This is done using TypeScript’s type intersection feature. For instance, you can merge a UUID with a “user” symbol to create a UserId. This ensures that UserId is treated differently from a plain string or other branded types like CarId.

What problem does this solve?
You can divide it into two parts:
- The TypeScript type system
- The consistency of your code and its improvement
If you decide to type your userId without intersection, the type check will accept all primitive types for your userId.

What is the problem with this example? The TypeScript type interpretation can’t differentiate between my UserId defined on line 3 and my CarId defined on line 4.
For me, it’s a real weakness because it allows the rest of the team to interchange variables without triggering any immediate error. At best, we’ll have to wait for the code review, or worse, until the pre-production tests…
Typing Intersection
To work with intersection typing in TypeScript, we use the & an operator like this:
type UserId = UUID & { readonly brand: unique symbol }
type CarId = UUID & { readonly brand: unique symbol }
Like the first schema, we get the piece of blue by mixing a UUID type and a unique symbol.
We need to modify the initialization of user and car to provide more detail about the type. I have wrapped the initialization inside two functions.
const createUser = (): UserId => {
return randomUUID() as UserId
}
const createCar = () :CarId => {
return randomUUID() as CarId
}
const user = createUser()
const car = createCar()t
And now, by design, we are protected against a switching variable error.

Moreover, when using branded types, you document your code, making it easier to read and onboard new developers.
If you take the printUserAndCar definition, it is very easy to understand. The first ID is a UserId, and the second is a CarId, so you know directly what each one represents.
Theory
I’ll be brief on this part as my math skills are not particularly strong. However, it seems important to cover some basic theories to demonstrate that using branded types is not just a whim.
Functional programming teaches us the link between programming and type theory. Type theory is a field of mathematics and computer science that deals with the classification of data types and their interactions. If you want to delve deeper, here is the Wikipedia link: https://en.wikipedia.org/wiki/Type_theory.
In our case, specifying types and their uniqueness limits the set of possible states and errors in our programs. Branded types enforce stricter type constraints, ensuring that different kinds of data cannot be mistakenly interchanged.
Imagine you have 15 unbranded types representing 10 different elements in your system. You might have a function with this signature:
function doSomething(id:UUID, id2:UUID, id3:UUID, id4:UUID){}
In this case, there are 50625 possible state combinations for this function call, as each parameter can be any of the 15 different UUIDs in your system. My calculation is as follows:
15 different UUIDs for each id parameter, leading to 15 15 15 * 15 combinations.
By design and theory, this function is very weak because it doesn’t adequately restrict the types.
Now, consider using branded UUIDs:
function doSomething(id:UserId, id2:CarId, id3:LicenceId, id4:BikeId)
Here, the 15 different UUIDs do not impact the number of possible states, as each parameter must match a specific branded type. Thus, the function has a single valid state: (UserId, CarId, LicenceId, BikeId).
By reducing the set of possible states that the function can accept, we greatly improve the quality and maintainability of our code. Branded types enforce strict type constraints, making our code more robust and reducing the likelihood of bugs.
Zod | Effect.Schema
I don’t know if all runtime typing checkers include this feature, but the two most commonly used ones have implemented it.
Here’s a small example for the Zod implementation: Zod Documentation
const Cat = z.object({ name: z.string() }).brand<"Cat">();
type Cat = z.infer<typeof Cat>;
const petCat = (cat: Cat) => {};
// this works
const simba = Cat.parse({ name: "simba" });
petCat(simba);
// this doesn't
petCat({ name: "fido" });
The definition of the brand is .brand<"Cat">().
As for Effect.schema, the implementation is quite advanced.
import { Brand } from "effect"
type Int = number & Brand.Brand<"Int">
const Int = Brand.refined<Int>(
(n) => Number.isInteger(n), // Check if the value is an integer
(n) => Brand.error(`Expected ${n} to be an integer`) // Error message if the value is not an integer
)
In this example, we can see that they define a Brand Int, and then they define the behavior they want.
And if you don’t use this solution, you have no excuse because TypeScript supports the branded type like that:
type UserId = UUID & { readonly brand: unique symbol }
Conclusion
I hope this article was enjoyable and helpful for your daily development work. It’s not very complicated to implement, and its impact on a project is enormous. If you liked the article, please follow me to stay updated on my future articles.