---
id: wiki-2026-0508-branded-types-for-nominal-typing
title: Branded Types for Nominal Typing
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [Nominal Types TypeScript, Opaque Types, Branded Types]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [typescript, types, nominal-typing, type-safety]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
  language: TypeScript
  framework: TS 5.x
---

# Branded Types for Nominal Typing

## 매 한 줄
> **"매 string 매 매 string 의 X"**. Branded types는 TypeScript의 structural type system 안에 nominal distinction을 흉내내는 trick — 동일한 underlying type (예: string)을 `UserId` vs `OrderId`처럼 incompatible 하게 만들어 mix-up을 compile time에 잡는다. 2026 거의 모든 fintech / id-heavy domain code에 standard.

## 매 핵심

### 매 동기
- TS는 structural — `{ name: string }` 두 개는 호환.
- Domain-specific id (UserId, OrderId, Email) 가 모두 plain string → 매 swap mistake 매 compile O.
- Brand 추가 → structurally distinct.

### 매 구현 패턴
- **Symbol-based brand**: `string & { readonly [BrandKey]: 'UserId' }`.
- **Intersection brand**: simple intersection.
- **TypeScript 5.x `unique symbol`**: 매 compile-only field — runtime cost zero.

### 매 응용
1. ID 매 strong typing (UserId vs OrderId vs SessionId).
2. Validated string (Email, URL) — runtime check 후 brand.
3. Unit type (Meter, Second) — physical unit safety.

## 💻 패턴

### Basic brand utility
```ts
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };

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

function userId(s: string): UserId { return s as UserId; }
function orderId(s: string): OrderId { return s as OrderId; }

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

const u = userId('u_123');
const o = orderId('o_456');

getUser(u);  // OK
getUser(o);  // Error: OrderId not assignable to UserId
getUser('u_123'); // Error: string not assignable to UserId
```

### Branded with validation (smart constructor)
```ts
type Email = Brand<string, 'Email'>;

function parseEmail(s: string): Email | null {
  return /^[^@]+@[^@]+\.[^@]+$/.test(s) ? (s as Email) : null;
}

function sendMail(to: Email, subj: string) { /* ... */ }

const e = parseEmail('a@b.com');
if (e) sendMail(e, 'Hi'); // 매 narrowed Email
```

### Unit-of-measure brand
```ts
type Meter = Brand<number, 'Meter'>;
type Second = Brand<number, 'Second'>;
type MeterPerSecond = Brand<number, 'm/s'>;

function speed(d: Meter, t: Second): MeterPerSecond {
  return (d / t) as MeterPerSecond;
}

const d = 100 as Meter;
const t = 9.58 as Second;
const v = speed(d, t);
// speed(t, d) — Error: Second not assignable to Meter
```

### Branded primitive with helper
```ts
function makeBrand<B extends string>() {
  return {
    of<T>(v: T): Brand<T, B> { return v as Brand<T, B>; },
    is(_v: unknown): _v is Brand<unknown, B> { return true; }
  };
}

const UserId = makeBrand<'UserId'>();
const OrderId = makeBrand<'OrderId'>();

const id = UserId.of('u_1');
```

### Zod integration
```ts
import { z } from 'zod';

const UserIdSchema = z.string().regex(/^u_/).brand<'UserId'>();
type UserId = z.infer<typeof UserIdSchema>;

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

const parsed = UserIdSchema.parse(req.params.id);
fetchUser(parsed);
```

### Discriminated brand for state machine
```ts
type Pending = Brand<{ status: 'pending'; id: string }, 'Pending'>;
type Approved = Brand<{ status: 'approved'; id: string; by: string }, 'Approved'>;
type Rejected = Brand<{ status: 'rejected'; id: string; reason: string }, 'Rejected'>;
type Order = Pending | Approved | Rejected;

function approve(o: Pending, by: string): Approved {
  return { ...o, status: 'approved', by } as Approved;
}
```

### Pattern: opaque type with module
```ts
// userId.ts
declare const brand: unique symbol;
export type UserId = string & { readonly [brand]: 'UserId' };
export const UserId = (s: string): UserId => {
  if (!s.startsWith('u_')) throw new Error('invalid');
  return s as UserId;
};
```

## 매 결정 기준
| 상황 | Approach |
|---|---|
| ID type collision risk | Brand 매 essential |
| Internal-only domain | Brand optional |
| Runtime validation needed | Smart constructor + brand |
| Schema parsing (Zod/Effect) | `.brand<'Name'>()` |
| Unit safety | Brand on number |

**기본값**: external boundary (DB, API) 에 entry point 에서 brand, internal logic은 branded type만 받음.

## 🔗 Graph
- 부모: [[TypeScript]] · [[TypeScript 타입 시스템 (TypeScript Type System)|Type System]]
- 변형: [[Opaque Types]]
- Adjacent: [[Zod]] · [[Effect-TS]]

## 🤖 LLM 활용
**언제**: domain model schema generation, brand utility scaffolding, smart constructor pattern.
**언제 X**: runtime brand check 매 X (compile-only) — runtime 필요 시 class / Symbol.

## ❌ 안티패턴
- **Brand 추가 후 무한 cast**: `as UserId` 남발 → safety 매 lost. Smart constructor 사용.
- **Brand on every type**: 매 friction 매 high — 매 boundary type 만.
- **Brand with mutable object**: 매 객체 매 변형 후 brand mismatch — readonly 사용.
- **Non-unique brand key**: `'id'` — collision risk. unique symbol 사용.

## 🧪 검증 / 중복
- Verified (Effect-TS docs, Zod 3.x, TypeScript handbook).
- 신뢰도 A.

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — branded type pattern + Zod integration |
