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

# Opaque Types (TypeScript)

## 매 한 줄
> **"매 structural type 의 nominal escape hatch"**. TypeScript 매 structural typing 이지만, opaque (branded) type 으로 매 string 끼리 / number 끼리 mix-up 방지. UserId vs PostId, Cents vs Dollars 등 매 domain primitive 의 type-safety 강화.

## 매 핵심

### 매 왜 필요
- 매 TS structural — `type UserId = string; type PostId = string;` 매 interchangeable.
- 매 runtime cost X 의 compile-time distinction 필요.
- 매 unit confusion (cents/dollars, ms/sec) 의 typing.

### 매 두 가지 구현
- **Symbol brand** (가장 strict, 매 declaration merging 어려움).
- **Unique string brand** (가장 ergonomic, TS 4.9+).

### 매 응용
1. ID types — UserId, PostId, OrgId 의 cross-mixing 방지.
2. Units — Meters vs Feet, Cents vs Dollars.
3. Validated strings — `Email`, `URL`, `SafeHtml` (validator 통과 후 만 cast).
4. Crypto — `HashedPassword` vs `PlainPassword`.

## 💻 패턴

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

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

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

const u: UserId = asUserId("u_123");
const p: PostId = asUserId("u_123"); // ❌ Type error
```

### Smart constructor with validation
```ts
type Email = Brand<string, "Email">;

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

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

send(makeEmail("a@b.co"), "hi"); // ✓
send("a@b.co" as string, "hi");   // ❌
```

### Numeric units
```ts
type Cents = Brand<number, "Cents">;
type Dollars = Brand<number, "Dollars">;

const toCents = (d: Dollars): Cents => (d * 100) as Cents;
const toDollars = (c: Cents): Dollars => (c / 100) as Dollars;

function charge(amount: Cents) { /* ... */ }
charge(toCents(9.99 as Dollars)); // ✓
charge(9.99 as Dollars);          // ❌
```

### Result-style smart constructor (no throw)
```ts
type ValidUrl = Brand<string, "ValidUrl">;

function parseUrl(s: string): ValidUrl | null {
  try {
    new URL(s);
    return s as ValidUrl;
  } catch {
    return null;
  }
}

const u = parseUrl(input);
if (u) fetch(u); // u: ValidUrl
```

### Generic Tagged
```ts
type Tagged<T, Tag extends string> = T & { readonly __tag: Tag };
type Meters = Tagged<number, "m">;
type Seconds = Tagged<number, "s">;
const speed = (m: Meters, s: Seconds): number => m / s;
```

### Zod + brand (runtime + compile-time)
```ts
import { z } from "zod";
const EmailSchema = z.string().email().brand<"Email">();
type Email = z.infer<typeof EmailSchema>; // string & z.BRAND<"Email">

const parsed = EmailSchema.parse(input); // Email
```

## 매 결정 기준
| 상황 | Approach |
|---|---|
| ID types | unique symbol brand |
| Validated input | smart constructor (parse, don't validate) |
| Runtime + compile | Zod `.brand<T>()` |
| Numeric units | Tagged number with conversion functions |
| One-off | inline `string & { _t: "X" }` |

**기본값**: unique-symbol brand + smart constructor — 매 boundary 의 cast, 매 internal 은 type-safe propagation.

## 🔗 Graph
- 부모: [[TypeScript-Type-System]]
- 변형: [[Branded-Types]]
- 응용: [[Parse-Dont-Validate]]
- Adjacent: [[API 응답 및 상태 모델링 (State Modeling and API Responses)|Discriminated-Unions]]

## 🤖 LLM 활용
**언제**: ID 매 mix 위험 / unit 매 confusion / validated string 의 boundary protection.
**언제 X**: 매 internal helper / one-off — 매 over-engineering.

## ❌ 안티패턴
- **Brand without smart constructor**: 매 `as` cast 매 어디서나 — type 의 의미 없음.
- **Erase brand**: `JSON.stringify` → parse 매 brand 손실 — boundary 에 re-validate.
- **Brand on `any`**: 매 base type 의 narrowing 효과 X.
- **Excessive branding**: 매 internal 의 모든 string 에 brand — readability 폭락.

## 🧪 검증 / 중복
- Verified (TypeScript Handbook, Effective TypeScript Item 37, Zod docs).
- 신뢰도 A.

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — symbol brand, smart constructor, Zod brand, units 패턴 |
