---
id: wiki-2026-0508-branded-types
title: Branded Types
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [Branded Types, Nominal Types, Opaque Types, Tagged Types]
duplicate_of: none
source_trust_level: A
confidence_score: 0.95
verification_status: applied
tags: [typescript, type-system, nominal-typing, domain-modeling]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
  language: typescript
  framework: typescript-5.x,zod,effect-ts
---

# Branded Types

## 매 한 줄
> **"매 structural type 의 nominal 의 emulate — 매 `string` 두 개 의 distinguish 의 enforce"**. TypeScript 의 structural typing 의 long-standing pain (UserId vs OrderId · Email vs string) 의 solve, 2026 의 Effect-TS Brand · Zod brand · Type-Fest Tagged 의 ergonomic API 의 mainstream.

## 매 핵심

### 매 problem
- TS structural: 매 `string` 의 모든 `string` 의 assignable.
- `function sendEmail(userId: string, email: string)` — 매 swap 의 type check pass.
- 매 nominal hint 의 require — but TS 의 native 의 no.

### 매 mechanism
- Intersection 의 phantom property: `string & { __brand: "UserId" }`.
- 매 runtime 의 plain string — 매 zero cost.
- Constructor function 의 cast — 매 single point 의 validate.

### 매 응용
1. Domain ID (UserId · OrderId · ProductId).
2. Validated string (Email · URL · UUID · NonEmptyString).
3. Numeric unit (Meters · Seconds · USD · Cents).
4. Sanitized input (HtmlSafeString · SqlEscapedString).

## 💻 패턴

### Vanilla TS brand
```typescript
type Brand<T, B> = T & { readonly __brand: B };

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

const UserId  = (s: string): UserId  => s as UserId;
const OrderId = (s: string): OrderId => s as OrderId;

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

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

getUser(u);    // OK
getUser(o);    // ❌ Type error — 매 nominal 의 distinguish
getUser("u-123"); // ❌ raw string 의 reject
```

### Branded with smart constructor (validate + brand)
```typescript
type Email = Brand<string, "Email">;

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

// safer Result variant
function tryEmail(s: string): { ok: true; value: Email } | { ok: false; error: string } {
  return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s)
    ? { ok: true,  value: s as Email }
    : { ok: false, error: "invalid email" };
}
```

### Zod brand (runtime + type)
```typescript
import { z } from "zod";

const UserIdSchema = z.string().uuid().brand<"UserId">();
type UserId = z.infer<typeof UserIdSchema>;

const id = UserIdSchema.parse("550e8400-e29b-41d4-a716-446655440000");
// id is branded UserId — 매 raw uuid string 의 reject elsewhere
```

### Effect-TS Brand
```typescript
import { Brand } from "effect";

type USD = number & Brand.Brand<"USD">;
const USD = Brand.refined<USD>(
  (n) => Number.isInteger(n) && n >= 0,
  (n) => Brand.error(`expected positive integer cents, got ${n}`),
);

const price = USD(1999);   // OK
const bad   = USD(-5);     // BrandErrors
```

### Numeric units (compile-time dimensional)
```typescript
type Meters  = Brand<number, "Meters">;
type Seconds = Brand<number, "Seconds">;
type MPS     = Brand<number, "MetersPerSecond">;

const m = (n: number) => n as Meters;
const s = (n: number) => n as Seconds;

function speed(d: Meters, t: Seconds): MPS {
  return (d / t) as MPS;
}

speed(m(100), s(9.58)); // OK
speed(s(9.58), m(100)); // ❌ swapped
```

### Type-Fest Tagged (lighter syntax)
```typescript
import type { Tagged } from "type-fest";

type Email  = Tagged<string, "Email">;
type UserId = Tagged<string, "UserId">;
```

### Phantom param (alternate encoding)
```typescript
declare const brand: unique symbol;
type Brand<T, K> = T & { readonly [brand]: K };
```

### Runtime parsing layer (input boundary)
```typescript
// API boundary — single point of validation
function handleRequest(raw: { userId: string; email: string }) {
  const userId = UserId(raw.userId);    // brand at boundary
  const email  = Email(raw.email);      // validate + brand
  return processOrder(userId, email);   // internal: branded everywhere
}
```

## 매 결정 기준
| 상황 | Encoding |
|---|---|
| Lightweight ID 의 distinguish | Vanilla `__brand` |
| Runtime validation + brand | Zod brand · Effect Brand |
| Functional pipeline · refinement | Effect-TS Brand |
| Library ergonomics | Type-Fest `Tagged` |
| Numeric unit · dimensional | Brand<number, "Unit"> |

**기본값**: domain ID 의 vanilla brand, validated value 의 Zod, numeric/refinement 의 Effect Brand.

## 🔗 Graph
- 부모: [[TypeScript Type System]]
- 변형: [[Nominal-Typing-in-TypeScript|Nominal Typing]] · [[Opaque Types]]
- 응용: [[Parse Don't Validate]]
- Adjacent: [[Ambient_Declarations]] · [[Schema Validation]]

## 🤖 LLM 활용
**언제**: domain ID 의 brand 의 generate, smart constructor + validator 의 draft, Zod schema 의 brand 의 add.
**언제 X**: 매 brand 의 over-applying — 매 internal helper 의 brand 의 X · 매 boundary 의 only.

## ❌ 안티패턴
- **Brand 의 everywhere**: function signature 의 noise 의 explode — 매 domain primitive 만.
- **Cast 의 sprinkled**: 매 single constructor 의 collapse — validation 의 single source.
- **Brand 의 leak through serialization**: JSON.parse 의 raw string 의 return — boundary parse 의 require.
- **Multiple brand 의 same value**: `Brand<string, "A"> & Brand<string, "B">` — 매 union 의 use 의 X · 매 새 brand 의 prefer.

## 🧪 검증 / 중복
- Verified (TypeScript handbook, Effect-TS docs, Zod 4 docs, Type-Fest Tagged docs, "Parse Don't Validate" Alexis King).
- 신뢰도 A.

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — vanilla + Zod + Effect-TS + Type-Fest variants |
