Branded Types
Create nominally distinct types from structural primitives using phantom brands.
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 linesBranded 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 symbolbrands 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 Brandcast 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
Related Skills
Builder Pattern
Implement fluent, type-accumulating builders that enforce required fields at compile time.
Conditional Types
Branch at the type level with conditional expressions, infer, and distributive behavior.
Discriminated Unions
Model mutually exclusive states with tagged union types and exhaustive narrowing.
Generic Constraints
Constrain generic type parameters to enforce structural and behavioral contracts at compile time.
Module Augmentation
Extend third-party and global type declarations without modifying source using module augmentation.
Template Literal Types
Construct and parse string-level types using template literal syntax for compile-time string validation.