Skip to main content
Technology & EngineeringTypescript Patterns126 lines

Branded Types

Create nominally distinct types from structural primitives using phantom brands.

Quick Summary20 lines
You are an expert in Branded Types for writing type-safe TypeScript.

## Key Points

- Always funnel branded value creation through a smart constructor or parsing function that validates invariants before casting.
- Use `unique symbol` brands for library-grade code to avoid brand collisions across packages.
- Keep branded types in a shared types module so that both producers and consumers import the same brand.
- Casting to a branded type without validation defeats the purpose; treat every `as Brand` cast as a trust boundary that must be justified.
- Attempting arithmetic or string operations directly on branded values requires casting back to the base type first, which can be verbose — provide helper functions for common operations.

## Quick Example

```typescript
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
```
skilldb get typescript-patterns-skills/Branded TypesFull skill: 126 lines
Paste into your CLAUDE.md or agent config

Branded Types — TypeScript Patterns

You are an expert in Branded Types for writing type-safe TypeScript.

Overview

TypeScript's type system is structural: two types with the same shape are interchangeable. Branded types (also called opaque or nominal types) attach a phantom property to a primitive or object type so that structurally identical values are treated as distinct by the compiler. Use branded types to prevent accidental mixing of IDs, units, validated strings, and other domain values that share a runtime representation but differ in meaning.

Core Philosophy

Branded types exist because TypeScript's structural type system, while powerful, cannot distinguish between values that share a runtime representation but carry different semantic meaning. A user ID and an order ID are both strings at runtime, yet passing one where the other is expected is a logic bug that structural typing alone cannot catch. Branded types close this gap by encoding domain meaning into the type system itself.

The key insight is that the "brand" property never exists at runtime — it is a phantom marker that lives only in the type layer. This means branded types have zero runtime cost while providing compile-time guarantees that domain boundaries are respected. The as cast inside a smart constructor is the single trust boundary where you assert that a value meets the brand's invariants, and every other part of the codebase benefits from that assertion without needing to repeat validation.

Think of branded types as a lightweight form of domain-driven design at the type level. Rather than creating full wrapper classes with methods and overhead, you get nominal distinction with the ergonomics of primitive values. This makes them ideal for IDs, validated strings, constrained numbers, and any value where "what it is" matters more than "what shape it has."

Core Concepts

Declaring a brand:

type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

Creating branded values through smart constructors:

function UserId(id: string): UserId {
  return id as UserId;
}

function OrderId(id: string): OrderId {
  return id as OrderId;
}

The compiler now prevents mixing:

function getUser(id: UserId): void { /* ... */ }

const uid = UserId("u-123");
const oid = OrderId("o-456");

getUser(uid);  // OK
getUser(oid);  // Error: OrderId is not assignable to UserId

Implementation Patterns

Validated email brand:

type Email = Brand<string, "Email">;

function parseEmail(input: string): Email {
  if (!/^[^@]+@[^@]+\.[^@]+$/.test(input)) {
    throw new Error(`Invalid email: ${input}`);
  }
  return input as Email;
}

Numeric units to prevent mixed arithmetic:

type Meters = Brand<number, "Meters">;
type Seconds = Brand<number, "Seconds">;

function speed(distance: Meters, time: Seconds): number {
  return (distance as number) / (time as number);
}

const d = 100 as Meters;
const t = 10 as Seconds;
speed(d, t);  // OK
speed(t, d);  // Error

Using unique symbol for a zero-cost brand (no runtime phantom field):

declare const __brand: unique symbol;

type Branded<T, B> = T & { [__brand]: B };

type PositiveInt = Branded<number, "PositiveInt">;

function toPositiveInt(n: number): PositiveInt {
  if (!Number.isInteger(n) || n <= 0) throw new Error("Must be positive integer");
  return n as PositiveInt;
}

Best Practices

  • Always funnel branded value creation through a smart constructor or parsing function that validates invariants before casting.
  • Use unique symbol brands for library-grade code to avoid brand collisions across packages.
  • Keep branded types in a shared types module so that both producers and consumers import the same brand.

Common Pitfalls

  • Casting to a branded type without validation defeats the purpose; treat every as Brand cast as a trust boundary that must be justified.
  • Attempting arithmetic or string operations directly on branded values requires casting back to the base type first, which can be verbose — provide helper functions for common operations.

Anti-Patterns

Scattering as Brand casts throughout the codebase. Every branded cast is a trust boundary. If you allow arbitrary code to cast raw values into branded types, you lose all guarantees. Funnel every branded value through a single smart constructor or parse function.

Branding without validation. A branded type that accepts any input without checking invariants is security theater. If Email is a brand, the constructor must validate the format; otherwise the brand conveys false confidence to downstream consumers.

Using branded types for values that need methods. If you find yourself writing many helper functions to operate on a branded primitive (add two Meters, concatenate two Path segments), a value object class with operator methods may be a better fit than a phantom brand.

Creating excessively granular brands. Branding every string and number in your domain creates casting friction that slows development without proportional safety gains. Brand the values that actually get mixed up in practice — IDs, validated inputs, and unit-sensitive numerics.

Exposing the brand's internal tag in public API contracts. The __brand property is an implementation detail. Do not rely on its name or structure in serialization, API responses, or cross-package contracts, because it does not exist at runtime and different packages may use different brand tag conventions.

Install this skill directly: skilldb add typescript-patterns-skills

Get CLI access →