C.W.K.
Stream
Lesson 03 of 05 · published

Discriminated Union: Tagged 패턴

~12 min · unions-intersections, discriminated-unions, tagged-unions, state-machines

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"TypeScript 의 가장 유용한 단일 패턴. 한 번 보면 매일 쓸 자리 찾게 돼."

Discriminated union 이 뭐

Discriminated union 은 모든 variant 가 그 variant 에 고유한 값의 literal 타입 property — discriminator — 담는 object 타입의 union. Compiler 가 너가 체크할 때 discriminator 써서 전체 object narrow.

고전 예시: 도형.

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number }
  | { kind: 'rect'; w: number; h: number };

모든 variant 가 그 variant 에 고유한 literal 문자열 값의 `kind` 필드. if (shape.kind === 'circle') 체크하면 compiler 가 `shape` 를 circle variant 로 narrow — 그리고 안전하게 `shape.radius` 읽을 수 있어.

왜 이 패턴이 너무 좋아

Discriminated union 이 실제 도메인 개념 깔끔하게 모델링: 상태 (idle/loading/done/error), event (click/keypress/scroll), API 응답 (success/error), 도형 (circle/square/rect). 데이터가 '이거 OR 저거, 어느 거냐에 따라 모양 달라' 인 어디든 discriminated union 이 옳은 모델.

결합 효과: compiler 가 variant 구별 유지, narrowing 이 `if`/`switch` 안에서 자동 발생, exhaustiveness 가 `never` 통해 강제 가능 (다음 lesson). 패턴이 2 variant 에서 20+ variant 까지 명확성 안 잃고 scale.

Discriminator 필드 관례

Discriminator 필드는 관례적으로 `kind`, `type`, `tag` 로 이름 붙음. 하나 골라서 codebase 전체에 일관되게 써. React 의 reducer 패턴이 `type` 써. 함수형-언어 port 자주 `tag` 써. 도메인-specific 프로젝트가 JavaScript 의 built-in `type` 의미와 충돌 피하려 `kind` 골라. 선택은 스타일 — 구체 이름보다 일관성 더 중요.

'다른 모양의 이것 OR 저것' 모델링하면, discriminated union 이 거의 확실히 옳은 모델. Enum 의 타입-안전, tagged record 의 구조, 일반 JavaScript object 의 ergonomics — 다 runtime 기계 없이.

피파의 고백

cwkPippa 의 거의 모든 state machine 이 discriminated union. Conversation lifecycle, council round state, streaming message phase, brain selection — 다 `kind` 나 `status` 로 tag. 패턴이 너무 default 라서 뭐가 안 그렇면 알아차려: '왜 이게 discriminated union 대신 optional 필드 가진 plain object 야?' 보통은 그래야 했어.

Code

도형 패턴·typescript
// 도형 — canonical 예시.
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number }
  | { kind: 'rect'; w: number; h: number };

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle': return Math.PI * s.radius ** 2;   // s 가 circle variant 로 narrow
    case 'square': return s.side ** 2;
    case 'rect':   return s.w * s.h;
  }
}

// 각 분기가 어느 variant 인지 정확히 앎.
// Compiler 가 추적: 'circle' 분기 안에서 s.radius 접근 가능; s.side 안 됨.
State machine 과 Redux action — 같은 패턴·typescript
// State machine.
type QueryState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function render<T>(state: QueryState<T>) {
  switch (state.status) {
    case 'idle':    return 'waiting';
    case 'loading': return 'spinner';
    case 'success': return `data: ${JSON.stringify(state.data)}`;     // state.data 만 여기서
    case 'error':   return `error: ${state.error.message}`;            // state.error 만 여기서
  }
}

// Redux-style action 타입.
type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET'; payload: number };

function reduce(state: number, action: Action): number {
  switch (action.type) {
    case 'INCREMENT': return state + 1;
    case 'DECREMENT': return state - 1;
    case 'SET':       return action.payload;     // payload 만 여기서
  }
}

External links

Exercise

Express-style HTTP handler 결과를 discriminated union 으로 모델링: { status: 'json'; body: unknown } | { status: 'redirect'; to: string } | { status: 'error'; code: number }. 그다음 status 에 switch 하고 각 case 처리하는 respond(res, result) 함수 써. Compiler 가 각 분기 안에서 variant narrow 하는지 확인.
Hint
status 의 switch 가 match 되는 분기에서만 body, to, code autocompletion 줘야 함. Redirect 분기에서 result.body 접근 시도하면 compiler 가 불평 — narrowing 작동하는 거.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.