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.

6 min read

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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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");
//    ^? Email

The 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:

typescripttypescript
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

ApproachCompile-time safetyRuntime overheadWorks with primitives
Branded typesYesNoneYes
Classes / wrappersYesYes (object allocation)No (must wrap)
Type aliases (type UserId = string)NoNoneYes
Enum-like string unionsPartialNoneYes (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:

typescripttypescript
type BrandOf<T> = T extends Brand<unknown, infer Tag> ? Tag : never;
 
type A = BrandOf<Email>;
//   ^? "Email"
 
type B = BrandOf<string>;
//   ^? never

The 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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What is a branded type in TypeScript?

A branded type is a primitive type (like `string` or `number`) combined with a unique tag property that exists only at compile time. It lets TypeScript distinguish between two strings that represent different concepts, like `UserId` and `OrderId`.

Does the brand exist at runtime?

No. The brand is purely a type-level construct. At runtime, a branded string is just a string. The brand prevents accidental mixing at compile time but adds zero runtime overhead.

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.