← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Branded / Opaque Types in TypeScript

typescript Advanced

Also Known As

branded types opaque types nominal typing TypeScript phantom types

TL;DR

A technique to make structurally identical types incompatible — a UserId and an OrderId are both strings, but branding makes them distinct types so passing an OrderId where UserId is expected is a compile-time error.

Explanation

TypeScript uses structural typing — two types with the same shape are interchangeable. 'type UserId = string' and 'type OrderId = string' are identical; any string satisfies either. Branded types add a phantom property that exists only at the type level, not at runtime: 'type UserId = string & { readonly __brand: unique symbol }'. Now UserId and OrderId are structurally different types. A plain string cannot be assigned to UserId without an explicit cast function, preventing accidental ID swaps. The brand property is never actually on the object — it is erased at runtime. A factory function performs the single trusted cast: 'function userId(id: string): UserId { return id as UserId; }'.

Common Misconception

Branded types add runtime overhead. The brand is a phantom — it exists only in the TypeScript type system and is completely erased at compile time. There is zero runtime cost.

Why It Matters

Without branded types, swapping a UserId and an OrderId in a function call is a silent bug — TypeScript accepts it because both are strings. With branded types, the compiler catches the swap immediately. This is especially valuable in PHP-backed APIs where IDs are all integers or UUIDs that look identical to TypeScript.

Common Mistakes

  • Using a string literal as the brand ('readonly __brand: 'UserId'') instead of unique symbol — string literals are structurally compatible across different branded types if they happen to share the same literal.
  • Performing the 'as' cast directly at call sites instead of in factory functions — the whole point is that the cast happens in one trusted place, not scattered through the codebase.
  • Not branding validated values — branded types are most powerful when the factory function validates the value: 'function email(s: string): Email { if (!isEmail(s)) throw new Error(); return s as Email; }'.
  • Over-branding — branding every string is noisy; use it for IDs, validated values, and domain-specific types where confusion would cause real bugs.

Code Examples

✗ Vulnerable
// ❌ Structural typing lets IDs be swapped silently
type UserId  = string;
type OrderId = string;

function getOrdersByUser(userId: UserId, orderId: OrderId): void {}

const uid: UserId  = 'user-123';
const oid: OrderId = 'order-456';

getOrdersByUser(oid, uid); // No error — both are strings!
✓ Fixed
// ✅ Branded types — ID swap is a compile error
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };

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

// Trusted factory functions — the only place the cast lives
const userId  = (id: string): UserId  => id as UserId;
const orderId = (id: string): OrderId => id as OrderId;

function getOrdersByUser(userId: UserId, orderId: OrderId): void {}

const uid = userId('user-123');
const oid = orderId('order-456');

getOrdersByUser(oid, uid); // TS Error: Argument of type 'OrderId' not assignable to 'UserId'
getOrdersByUser(uid, oid); // ✓ Correct

Added 23 Mar 2026
Views 26
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 1 ping M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T
No pings yet today
No pings yesterday
Amazonbot 8 Perplexity 4 Google 2 Ahrefs 2 ChatGPT 1
crawler 16 crawler_json 1
DEV INTEL Tools & Severity
⚙ Fix effort: Medium
⚡ Quick Fix
Create a branding helper: 'type Brand<T, B> = T & { readonly __brand: B }'. Then 'type UserId = Brand<string, 'UserId'>'. Create factory functions that do the single trusted cast from unbranded to branded values.

✓ schema.org compliant