Branded Types (Nominal Typing)
Branded types add invisible stamps to structurally identical types, preventing them from being mixed up.
TypeScript uses a structural type system — if two types have the same shape, they are considered the same type. This is powerful and flexible, but it creates a problem: what if two types have the same structure but represent COMPLETELY DIFFERENT concepts? A UserId and an OrderId might both be strings, but they are NOT the same thing. Passing an OrderId to a function that expects a UserId is a bug — but structural typing won't catch it.
Think of currency notes in Hyderabad. A 500 rupee note from India and a 500 rupee note from Nepal look very similar — both are rectangular paper notes with numbers and security features. Structurally, they are identical. But they have DIFFERENT STAMPS on them — one says "Reserve Bank of India" and the other says "Nepal Rastra Bank." Those stamps make them distinct, even though their physical structure is nearly identical. You cannot use a Nepali rupee note in a Hyderabad shop — the shopkeeper checks the stamp.
Branded types add an INVISIBLE STAMP to your TypeScript types. type UserId = string & { readonly __brand: unique symbol } is structurally a string, but it has an invisible brand that says "I am a UserId." If you try to pass an OrderId (which is also a string with a different brand), TypeScript catches the mismatch at compile time. The brand has zero runtime cost — it's erased after compilation. It's like an invisible stamp that only the TypeScript compiler can see, ensuring that even structurally identical types are kept separate in your code.
A branded type is an intersection of a base type with an object containing a unique brand property:
// Define branded types
type UserId = string & {
readonly __brand: unique symbol;
};
type OrderId = string & {
readonly __brand: unique symbol;
};
// Functions that accept branded types
function findUser(
id: UserId
): User {
// id is a string at runtime
console.log(
"Looking up user:", id
);
return { id, name: "Imran" };
}
function findOrder(
id: OrderId
): Order {
console.log(
"Looking up order:", id
);
return { id, items: [] };
}
// Creating branded values
const userId =
"user-123" as UserId;
const orderId =
"order-456" as OrderId;
// This WORKS:
findUser(userId); // ✅
findOrder(orderId); // ✅
// This FAILS at compile time:
findUser(orderId); // ❌ Type 'OrderId'
// is not assignable to 'UserId'
findOrder(userId); // ❌ Type 'UserId'
// is not assignable to 'OrderId'
Even though both UserId and OrderId are string under the hood, TypeScript treats them as DISTINCT types because of the brand. The brand property (__brand) is a phantom type — it exists only at the type level and has no runtime representation.
Branded types can be applied to any base type, not just strings:
// Branded numbers
type PositiveInt = number & {
readonly __brand: "PositiveInt";
};
// Branded objects
type SanitizedHTML = string & {
readonly __brand: "SanitizedHTML";
};
// Branded arrays
type NonEmptyArray<T> = T[] & {
readonly __brand: "NonEmptyArray";
};Here's how to effectively use branded types in real-world TypeScript code.
Factory Functions with Validation
// A factory function that creates
// branded types with validation
function createUserId(
id: string
): UserId | Error {
if (!/^user-[a-f0-9]+$/
.test(id)) {
return new Error(
"Invalid user ID format"
);
}
return id as unknown as UserId;
}
function createOrderId(
id: string
): OrderId | Error {
if (!/^ord-[a-f0-9]+$/
.test(id)) {
return new Error(
"Invalid order ID format"
);
}
return id as unknown as OrderId;
}
// Usage with validation
const userId =
createUserId("user-abc123");
const orderId =
createOrderId("ord-xyz789");
Branded Types with Classes
class Email {
constructor(
public readonly value: string
) {
if (!this.isValid(value)) {
throw new Error(
"Invalid email"
);
}
}
private isValid(email: string) {
return /^[^@]+@[^@]+$/
.test(email);
}
// Prevent structural typing
private __brand!: void;
}
function sendEmail(
to: Email,
subject: string,
body: string
): void {
// to is guaranteed valid!
}
Generic Brand Helper
// Generic utility for creating
// branded types
declare const BRAND: unique symbol;
type Brand<T, B> = T & {
readonly [BRAND]: B;
};
type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;
type Email = Brand<string, "Email">;
type PositiveInt = Brand<number, "PosInt">;Common branded type pitfalls to avoid.
Trap 1: Runtime Brand Access
You cannot check brands at runtime — they're erased. Use wrapper types or classes if runtime validation is needed.
Trap 2: Casting Without Validation
Type assertions bypass validation. Always validate data in factory functions before asserting the brand.
Trap 3: Over-Branding
Not every string needs a brand. Use branded types only for values that cross meaningful boundaries (API boundaries, database IDs, user input).
Branded Type Definition:
declare const BRAND: unique symbol;
type Brand<T, B> = T & { readonly [BRAND]: B };
type UserId = Brand<string, "UserId">;
Key Rules:
- Branded types prevent structural type confusion at compile time
- Use phantom brand properties (unique symbol or literal) — zero runtime cost
- Always validate in factory functions before casting to branded types
- Use brands at API boundaries, database IDs, and security-sensitive values
- Don't over-brand — only brand types that have meaningful conceptual differences
The Golden Rule: "Branded types are like invisible currency stamps — they don't change the value itself, but they ensure you use the right type in the right place. UserId and OrderId may both be strings, but they are NOT the same thing. Trust the stamp, bhai!"
Key Takeaways
- Branded types prevent accidentally mixing structurally identical types (UserId vs OrderId)
- Defined as intersection types: type X = T & { readonly __brand: unique symbol }
- The brand is a phantom type — erased at runtime, zero cost
- Always use factory functions with validation to create branded values
- Use at API boundaries, database IDs, and any security-sensitive identifiers
- Consider the generic Brand
helper for reusable brand definitions
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login