What is branded types?
Branded types are a powerful TypeScript technique used to create 'nominal' types within its 'structural' type system. They allow you to differentiate between types that are structurally identical but semantically distinct, significantly improving type safety and preventing common programming errors.
What are Branded Types?
In TypeScript, two types are compatible if their structures match (structural typing). For example, a string can be assigned to a variable expecting a UserId if UserId is just a type alias for string. Branded types address this by adding a unique, non-existent, and typically private property (the 'brand') to an otherwise structurally identical type. This 'brand' property makes the types structurally different in the eyes of the TypeScript compiler, even though it doesn't exist at runtime.
The core idea is to intersect the base type (e.g., string, number) with an object type that contains a literal string property, often __brand or _type, ensuring it will never clash with real data. This 'brand' serves purely as a compile-time differentiator.
Why Use Branded Types?
- Enhanced Type Safety: Prevents accidental assignment of a value that has the correct underlying type but represents a different semantic entity (e.g., passing a
productIdto a function expecting auserId). - Clarity and Readability: Makes the intent of your types explicit, leading to more understandable code.
- Domain-Specific Constraints: Enforces domain rules at compile-time. For instance, an ID that must be a string but specifically represents a user ID, not just any string.
- Refactoring Safety: Reduces the risk of introducing bugs during refactoring by catching type mismatches earlier.
How to Implement Branded Types
The common pattern involves defining a type that is an intersection of the base type (e.g., string or number) and an object literal with a unique, never-used property, often a symbol or a literal string property like __brand or _type. This property is usually set to never or a unique string literal to prevent runtime conflicts and ensure it's only a compile-time construct.
type Brand<K, T> = K & { __brand: T };
// Define branded types
type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;
type OrderId = Brand<string, 'OrderId'>;
// Helper function to create branded types safely (optional, but good practice)
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
// Example usage
const userId = createUserId('user-123');
const productId = createProductId('prod-abc');
function processUser(id: UserId): void {
console.log(`Processing user: ${id}`);
}
function processProduct(id: ProductId): void {
console.log(`Processing product: ${id}`);
}
processUser(userId); // OK
// processUser(productId); // Type Error: Argument of type 'ProductId' is not assignable to parameter of type 'UserId'.
// A raw string cannot be directly assigned to a branded type
const rawId = 'some-id';
// processUser(rawId); // Type Error: Argument of type 'string' is not assignable to parameter of type 'UserId'.
Analogy
Think of different types of keys that might all be made of metal and have similar shapes (structurally identical), but each key has a tiny, unique engraving or a specific color (the 'brand') that indicates which lock it opens. You wouldn't accidentally use a 'house key' to open your 'car lock,' even if they were both physically similar, because you recognize their distinct purposes due to their 'brand.' Branded types bring this level of distinction to your code.
Considerations
- Runtime Overhead: Branded types are purely a compile-time construct and have no runtime impact. The 'brand' property is never actually added to the JavaScript object.
- Type Assertions: You often need type assertions (
as BrandedType) when converting a base type to its branded counterpart, which should be done carefully and only when you're sure of the type's semantic meaning. - Overuse: While powerful, branded types should be used judiciously. Not every
stringornumberneeds to be branded; reserve them for cases where semantic distinction is critical.