SyntaxStudy
Sign Up
TypeScript Branded Types and Opaque Types
TypeScript Beginner 1 min read

Branded Types and Opaque Types

TypeScript's structural type system means two types with identical shapes are interchangeable, even if they represent semantically different concepts. A User ID and an Order ID might both be numbers, but assigning one where the other is expected is a programming mistake. Branded types (also called nominal types or opaque types) solve this by adding a phantom brand property that distinguishes the types at the type level without existing at runtime. The branding technique uses an intersection with an object type containing a unique symbol or a never-accessible string property. The brand is a compile-time fiction — no actual property is added to the value. You use a factory function or type assertion to create branded values and regular type annotations to accept them. This creates a lightweight nominal type system on top of TypeScript's structural one. Branded types are particularly valuable for validated values — parsed email addresses, sanitised HTML, validated UUIDs, currency amounts with specific units, and measurement values with specific units. By returning a branded type from a validation function, you encode the fact that the value has been validated directly into the type system, making it impossible to accidentally use an unvalidated value where a validated one is required.
Example
// Brand utility type
type Brand<T, B extends string> = T & { readonly __brand: B };

// Branded primitive types
type UserId    = Brand<number, "UserId">;
type OrderId   = Brand<number, "OrderId">;
type Email     = Brand<string, "Email">;
type USD       = Brand<number, "USD">;
type EUR       = Brand<number, "EUR">;

// Factory functions that create branded values
function asUserId(n: number):  UserId  { return n as UserId; }
function asOrderId(n: number): OrderId { return n as OrderId; }

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

// Functions that only accept the correct branded type
function fetchUser(id: UserId): Promise<string> {
    return Promise.resolve(`User ${id}`);
}

const userId  = asUserId(1);
const orderId = asOrderId(1);

fetchUser(userId);  // OK
// fetchUser(orderId);  // Error: OrderId is not assignable to UserId
// fetchUser(1);        // Error: number is not assignable to UserId

// Currency brands prevent mixing
function addUSD(a: USD, b: USD): USD { return (a + b) as USD; }

const price: USD  = 19.99 as USD;
const tax: USD    = 1.60  as USD;
const total       = addUSD(price, tax);

const eur: EUR = 18.00 as EUR;
// addUSD(price, eur); // Error: EUR is not assignable to USD