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

Branded / Opaque Types in TypeScript

TypeScript Advanced
debt(d1/e3/b3/t5)
d1 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'caught instantly (compiler/syntax error)' (d1). The entire purpose of branded types is to make misuse a compile-time error — TypeScript's type checker rejects a UserId where an OrderId is expected immediately, with no extra tooling required.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix (replace pattern with safer alternative)' (e3). The quick_fix describes creating a reusable Brand<T,B> helper type and factory functions. This is a small, contained refactor: define the helper, define the branded type aliases, and write factory functions — all within a type definitions file or domain module, not a cross-cutting change.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax (one component pays, rest of codebase unaffected)' (b3). Branded types are opt-in and scoped to the domain types where they are applied. Files that use branded types must go through factory functions, but the pattern doesn't impose structural constraints on the rest of the codebase. Over-branding (a noted common mistake) could raise this, but typical disciplined use stays localised.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The canonical misconception is that branded types add runtime overhead, but they are fully erased at compile time. Additionally, a common mistake is using string literals instead of unique symbols as brands, which causes structural compatibility between different branded types — a subtle gotcha that undermines the whole point of branding and is non-obvious until encountered.

About DEBT scoring →

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 58
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 1 ping T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 2 pings T 1 ping F 1 ping S 1 ping S 1 ping M 1 ping T 2 pings W 1 ping T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 0 pings F 1 ping S 0 pings S 2 pings M 1 ping T 0 pings W
No pings yet today
SEMrush 1
Amazonbot 10 Scrapy 8 Google 4 Perplexity 4 Ahrefs 4 SEMrush 3 PetalBot 3 Claude 2 ChatGPT 1 Bing 1 Meta AI 1
crawler 38 crawler_json 3
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