---
id: typescript-branded-types
title: TypeScript Branded Types — Nominal 타이핑
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [typescript, types, nominal, vibe-coding]
tech_stack: { language: "TypeScript 5+", applicable_to: ["Web", "Backend"] }
applied_in: []
aliases: [nominal types, opaque types, brand]
---

# Branded Types — Nominal 타이핑

> TypeScript 는 structural. `string` 은 어떤 string 도 다 같다. **`UserId` vs `OrderId` 를 컴파일러가 구분**하려면 brand 를 부여. 런타임 비용 0.

## 📖 핵심 개념
Brand = 컴파일 타임에만 존재하는 phantom 필드. 같은 underlying type 이지만 다른 brand 를 단 두 타입은 호환 안 됨.

## 💻 코드 패턴

```ts
// 정의
type Brand<T, B> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type Email = Brand<string, 'Email'>;

// 생성 (검증 포함)
function asUserId(s: string): UserId {
  if (!/^u_[a-z0-9]{16}$/.test(s)) throw new Error('Invalid UserId format');
  return s as UserId;
}
function asEmail(s: string): Email {
  if (!s.includes('@')) throw new Error('Invalid email');
  return s.toLowerCase() as Email;
}

// 사용
function findOrder(id: OrderId) { ... }
const u: UserId = asUserId('u_abc...');
findOrder(u); // ❌ 컴파일 에러 — UserId 는 OrderId 가 아님
findOrder('arbitrary'); // ❌ string 도 OrderId 아님
```

## 🤔 의사결정 기준
| 도메인 값 | brand 필요 |
|---|---|
| 여러 종류의 ID (UserId, OrderId, ProductId) | ✅ |
| 검증된 후의 값 (Email, URL, NonEmptyString) | ✅ |
| 단위 (Meters, Feet, Cents) | ✅ — 잘못된 산술 차단 |
| 일회성 단순 string | ❌ |
| API 경계 응답 raw — 즉시 brand 변환 | 경계에서 검증 + brand |

## ❌ 안티패턴
- **`as UserId` 캐스팅 만 하고 검증 없음**: brand 의미 없음. 잘못된 형식의 string 이 통과.
- **brand 없이 `type UserId = string`**: alias 일 뿐 — `string` 어디서나 통과.
- **brand 객체에 추가 메서드**: brand 는 phantom 이어야 함. 메서드는 별도 함수.
- **brand 만들기는 했는데 internal 에서 plain string 으로 다루기**: 경계는 brand, 내부도 brand. 일관 유지.
- **runtime 객체에 `__brand: 'UserId'` 실제 추가**: 메모리 낭비 + 직렬화 오염. brand 는 타입에만.

## 🤖 LLM 활용 힌트
- 도메인 모델 작성 시 "ID / 검증된 값은 모두 branded type 으로" 명시.
- 생성 함수 (`asXxx`) 가 검증 포함하도록 강제.
- API 응답 zod 스키마와 결합: `z.string().email().brand<'Email'>()`.

## 🔗 관련 문서
- [[TypeScript_Type_Predicates]]
- [[Tagged_Union_Discriminated_Types]]
