Branded Types in TypeScript
Branded types add compile-time tags to primitives so TypeScript can tell apart values that share the same underlying type, like UserId and OrderId.
TypeScript uses structural typing. Two types are compatible if their shapes match, regardless of their names. This is usually convenient, but for domain primitives like UserId and OrderId, you want the opposite: you want the compiler to reject a UserId where an OrderId is expected, even though both are just strings.
TypeScript branded types solve this. A branded type is a primitive plus a compile-time tag that makes it unique:
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
function getUser(id: UserId) {
/* lookup user */
}
const userId = "usr_123" as UserId;
const orderId = "ord_456" as OrderId;
getUser(userId); // ok
getUser(orderId);
// ^? Argument of type 'OrderId' is not assignable to parameter of type 'UserId'.
// Types of property '__brand' are incompatible.
// Type '"OrderId"' is not assignable to type '"UserId"'.Both UserId and OrderId are strings at runtime. But at compile time, TypeScript sees the __brand property and treats them as incompatible. You cannot accidentally swap them.
How branded types work
Every branded type follows the same shape, so it helps to see the general pattern before looking at more examples. The pattern is an intersection of a base type with an object containing a brand property:
type Brand<Base, BrandName> = Base & { readonly __brand: BrandName };This creates a type that behaves like the base type in every way. You can call .toUpperCase() on a branded string or do arithmetic on a branded number, but you cannot assign one brand where a different brand is expected.
A helper type makes branding consistent across your codebase:
type Brand<Base, Tag> = Base & { readonly __brand: Tag };
type Email = Brand<string, "Email">;
type SanitizedHtml = Brand<string, "SanitizedHtml">;
type Meters = Brand<number, "Meters">;
type Seconds = Brand<number, "Seconds">;You can use branded types anywhere a plain primitive would go, but TypeScript prevents cross-brand assignments.
Creating branded values
Since the brand does not exist at runtime, you create branded values through factory functions that assert the brand:
type Email = Brand<string, "Email">;
function createEmail(raw: string): Email {
if (!raw.includes("@")) {
throw new Error("Invalid email");
}
return raw as Email;
}
const email = createEmail("alice@example.com");
// ^? EmailThe runtime validation happens inside createEmail. The type assertion as Email tells TypeScript to trust that this string is now an Email. This is one of the rare places where a type assertion is the right tool, since you are adding type information that cannot be verified structurally.
Alternatively, use a constructor-like helper function instead of casting inline at every call site. This keeps the as assertion in one place and lets every branded value go through the same entry point:
function brand<T extends Brand<unknown, unknown>>(base: Unbrand<T>, tag: T["__brand"]): T {
return base as T;
}
type Unbrand<T> = T extends Brand<infer Base, unknown> ? Base : never;
const meters: Meters = brand(100, "Meters");Unbrand uses infer inside a conditional type to extract the base type from a branded type. For more on the capture mechanism, see Infer Keyword in TypeScript.
Comparing branded types to alternatives
| Approach | Compile-time safety | Runtime overhead | Works with primitives |
|---|---|---|---|
| Branded types | Yes | None | Yes |
| Classes / wrappers | Yes | Yes (object allocation) | No (must wrap) |
| Type aliases (type UserId = string) | No | None | Yes |
| Enum-like string unions | Partial | None | Yes (but not unique) |
Plain type aliases like type UserId = string offer zero protection. TypeScript treats UserId and string as interchangeable because they are structurally identical.
Classes give nominal typing but add runtime overhead. Every ID becomes a heap-allocated object, and you lose the ability to use it directly as a primitive.
Branded types give you the protection of nominal typing with the performance of primitives.
Branded types and conditional types
You can use conditional types with branded types to extract or match brands. This is useful when you need to read the tag back out of a branded value or write a helper that only accepts a specific brand:
type BrandOf<T> = T extends Brand<unknown, infer Tag> ? Tag : never;
type A = BrandOf<Email>;
// ^? "Email"
type B = BrandOf<string>;
// ^? neverThe same idea also works with distributive conditional types. Instead of extracting the brand, you check each member of a union against a target brand and keep only the matches:
type OfBrand<T, Tag> = T extends Brand<unknown, Tag> ? T : never;
type DomainTypes = Email | SanitizedHtml | Date;
type StringBrands = OfBrand<DomainTypes, string>;For more on this filtering pattern, see Distributive Conditional Types in TypeScript.
Practical patterns
The patterns above are building blocks. In real code, branded types usually show up in two places: numeric units that are easy to mix up, and strings that need validation before use.
Branded number types for units:
type Meters = Brand<number, "Meters">;
type Kilometers = Brand<number, "Kilometers">;
function kmToMeters(km: Kilometers): Meters {
return (km * 1000) as Meters;
}
const distance: Kilometers = 5 as Kilometers;
const inMeters: Meters = kmToMeters(distance);Without branded types, both would be plain numbers and you could pass meters where kilometers are expected.
Branded strings for validated input:
type NonEmptyString = Brand<string, "NonEmpty">;
type TrimmedString = Brand<string, "Trimmed">;
function nonEmpty(raw: string): NonEmptyString {
if (raw.trim().length === 0) throw new Error("Empty string");
return raw as NonEmptyString;
}
function trimmed(raw: string): TrimmedString {
return raw.trim() as TrimmedString;
}These brands encode validation state in the type system. A function that requires a NonEmptyString signals to callers that they must validate first.
Limitations
No runtime enforcement. The brand is purely a type-level construct. If someone casts "anything" as UserId, TypeScript trusts them. Brands prevent accidents, not malice.
No cross-module brand uniqueness by default. Two modules could independently define type UserId = Brand<string, "UserId"> and produce compatible types. Namespace your brand tags, such as Brand<string, "auth/UserId">, or use unique-symbol-based brands for stronger uniqueness.
Symbol-based brands for stronger isolation:
declare const UserIdBrand: unique symbol;
type UserId = string & { readonly [UserIdBrand]: true };A unique symbol cannot be duplicated across modules, so this guarantees brand uniqueness. The trade-off is slightly more verbose syntax.
Rune AI
Key Insights
- Branded types use an intersection with a unique tag to distinguish primitives.
- The brand property only exists at compile time, never at runtime.
- Use branded types for IDs, emails, validated strings, and domain primitives.
- Factory functions with type assertions create branded values from plain primitives.
- For advanced patterns, combine branded types with conditional types and
infer.
Frequently Asked Questions
What is a branded type in TypeScript?
Does the brand exist at runtime?
Conclusion
Branded types give you nominal typing for primitives without any runtime cost. Add a unique __brand property at the type level, and TypeScript will prevent you from accidentally passing a UserId where an OrderId is expected. They are one of the lightest-weight ways to add domain safety to a TypeScript codebase.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.