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

# Nominal Typing in TypeScript

## 매 한 줄
> **"매 TypeScript 매 structural-type 시스템 매 nominal 효과 매 simulation — 매 phantom field 매 'brand' 매 통해 매 동일 shape 매 다른 type 으로 separate"**. TS 매 native nominal X 매 (Java/C# 의 `class A {} class B {}` 매 다름) — 매 brand pattern 매 community-standard, 매 ID/Currency/Email 매 같은 매 primitive 매 mix-up bug 매 compile-time 차단.

## 매 핵심

### 매 Structural vs Nominal
- **Structural** (TS, Go, OCaml): 매 shape 매 동일하면 매 동일 type — `interface A { x: number }` ≡ `interface B { x: number }`.
- **Nominal** (Java, C#, Rust): 매 declared name 매 동일해야 — 매 동일 shape 매도 매 다른 type 이면 incompatible.

### 매 brand pattern 종류
- **Class private field**: 매 `#brand` — 매 nominal 한 hint, 매 runtime cost.
- **Intersection with brand**: `T & { __brand: 'X' }` — 매 zero runtime, 매 compile-time only.
- **Symbol brand**: `T & { [brand]: 'X' }` — 매 collision-safe.
- **Unique symbol**: `declare const brand: unique symbol` — 매 strongest TS pattern.
- **TypeScript 5.0+ `type` operator** (proposed): 매 still community pattern.

### 매 응용
1. ID type separation: `UserId` vs `OrderId` 매 mix 차단.
2. Validated values: `Email`, `Url`, `NonEmptyString`.
3. Units: `Meters`, `Seconds`, `USD` vs `EUR`.
4. Sanitized strings: `SafeHtml`, `SqlEscaped`.
5. Domain primitives (DDD value object 매 lightweight 표현).

## 💻 패턴

### Basic brand
```ts
type Brand<T, B> = T & { readonly __brand: B }

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

const userId = 'u_123' as UserId
const orderId = 'o_456' as OrderId

function getUser(id: UserId) { /* */ }
getUser(userId)  // OK
getUser(orderId) // 매 compile error: OrderId not assignable to UserId
getUser('u_123') // 매 compile error: string not assignable to UserId
```

### Smart constructor (validation)
```ts
type Email = Brand<string, 'Email'>

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

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

const e = makeEmail('a@b.com')
if (e) sendMail(e, 'hi') // 매 narrowed to Email
sendMail('raw' as string, 'hi') // 매 compile error
```

### Unique symbol brand (strongest)
```ts
declare const userIdBrand: unique symbol
declare const orderIdBrand: unique symbol

type UserId = string & { readonly [userIdBrand]: void }
type OrderId = string & { readonly [orderIdBrand]: void }

// 매 여기서는 매 두 type 매 절대 호환 X (unique symbol 매 collision-free)
```

### Reusable Brand utility
```ts
declare const brandSymbol: unique symbol
type Brand<T, Tag extends string> = T & { readonly [brandSymbol]: Tag }

type UserId = Brand<string, 'UserId'>
type Meters = Brand<number, 'Meters'>
type Seconds = Brand<number, 'Seconds'>

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

const d = 100 as Meters
const t = 5 as Seconds
speed(d, t)  // OK
speed(t, d)  // 매 compile error
```

### Class private field (alternative)
```ts
class UserId {
  #brand!: void
  constructor(public readonly value: string) {}
}
class OrderId {
  #brand!: void
  constructor(public readonly value: string) {}
}

function getUser(id: UserId) { /* */ }
getUser(new UserId('u_1'))   // OK
getUser(new OrderId('o_1'))  // 매 compile error: #brand 매 only-accessible
// 매 cost: 매 wrapping object — 매 runtime allocation
```

### Sanitized string
```ts
type SafeHtml = Brand<string, 'SafeHtml'>

function escapeHtml(raw: string): SafeHtml {
  return raw.replace(/[&<>"']/g, (c) => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
  }[c]!)) as SafeHtml
}

function render(html: SafeHtml) { /* dangerouslySetInnerHTML OK */ }
render('<script>' as SafeHtml) // 매 cast 매 강제 — 매 reviewer 매 catch
render(escapeHtml(userInput))  // 매 safe path
```

### Currency
```ts
type USD = Brand<number, 'USD'>
type EUR = Brand<number, 'EUR'>

function add(a: USD, b: USD): USD { return (a + b) as USD }

const price = 100 as USD
const tax = 7 as USD
const total = add(price, tax) // OK

const eu = 50 as EUR
add(price, eu) // 매 compile error
```

### Phantom type with generics
```ts
type Validated<T, V extends string> = T & { readonly __validated: V }

type Trimmed<T extends string> = Validated<T, 'Trimmed'>
type NonEmpty<T extends string> = Validated<T, 'NonEmpty'>

function trim<T extends string>(s: T): Trimmed<T> {
  return s.trim() as Trimmed<T>
}

function requireNonEmpty<T extends string>(s: T): NonEmpty<T> {
  if (s.length === 0) throw new Error('empty')
  return s as NonEmpty<T>
}
```

### Library: ts-brand / Effect-TS
```ts
// 매 ts-brand
import { Brand, make } from 'ts-brand'

type UserId = Brand<string, 'UserId'>
const UserId = make<UserId>()
const id = UserId('u_1')

// 매 Effect-TS Schema
import { Schema } from '@effect/schema'

const UserId = Schema.String.pipe(Schema.brand('UserId'))
type UserId = Schema.Schema.Type<typeof UserId>
```

### JSON deserialization 매 brand 복원
```ts
interface UserJson { id: string; name: string }
interface User { id: UserId; name: string }

function fromJson(j: UserJson): User {
  return { id: j.id as UserId, name: j.name }
}
// 매 boundary 매서만 cast — 매 internal 매 type 안전
```

## 매 결정 기준
| 상황 | Approach |
|---|---|
| ID separation | Brand<string, 'XId'> |
| Validated value (Email, Url) | Brand + smart constructor |
| Unit (Meters, USD) | Brand<number, 'Unit'> |
| Sanitized string | Brand<string, 'Safe'> + escape function |
| Strongest isolation | unique symbol brand |
| Runtime check 동반 | Class with #brand + factory |
| 매 library 사용 OK | ts-brand / Effect Schema |
| Domain layer 매 rich behavior | Class wrapper (DDD value object) |

**기본값**: 매 `Brand<T, 'Name'>` intersection — 매 zero runtime cost + 매 sufficient nominal effect.

## 🔗 Graph
- 부모: [[TypeScript]] · [[TypeScript 타입 시스템 (TypeScript Type System)|Type-System]]
- 변형: [[Branded-Types]] · [[Opaque-Types]]
- 응용: [[Domain-Driven-Design]] · [[Value-Object]] · [[Refinement-Type]]
- Adjacent: [[Effect-TS]] · [[Zod]] · [[Type-Safety]] · [[Smart-Constructor]]

## 🤖 LLM 활용
**언제**: 매 ID/string mix-up 매 bug surface 자주, 매 unit/currency 혼동 risk, 매 sanitized vs raw 매 boundary 강제, 매 DDD value object 매 lightweight 표현.
**언제 X**: 매 throwaway code, 매 internal-only types 매 1 곳만 사용 (overhead), 매 simple CRUD CRUD 매 brand 폭발.

## ❌ 안티패턴
- **매 모든 string 매 brand**: 매 cognitive overhead 폭증 — 매 confusion-prone boundary 만 brand.
- **`as Brand` 매 무절제 cast**: 매 brand 의 의미 없음 — 매 smart constructor 매 통과 필수.
- **Brand 매 runtime check 가정**: 매 compile-time only — 매 deserialization 매 raw cast 시 brittle.
- **매 deeply nested generic brand**: 매 type error message 매 unreadable.
- **`__brand` field 매 runtime expose**: 매 JSON serialize 시 leak — 매 phantom field 매 절대 assign X.

## 🧪 검증 / 중복
- Verified (TypeScript handbook discussion, Effect-TS Schema, ts-brand library).
- 신뢰도 A.

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — brand patterns + smart constructor + unit/currency examples |
