--- id: wiki-2026-0508-discriminated-unions title: Discriminated Unions category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Tagged Unions, Sum Types, Algebraic Data Types, ADT] duplicate_of: none source_trust_level: A confidence_score: 0.95 verification_status: applied tags: [typescript, types, functional, type-narrowing] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: typescript framework: type-system --- # Discriminated Unions ## 매 한 줄 > **"매 한 literal field 의 매 type narrow"**. 매 TS / F# / Rust enum / Haskell ADT 의 same idea — 매 union 의 each variant 의 unique discriminant (tag) 의 carry, 매 compiler 의 매 switch 시 매 narrow. 매 2026 TS 5.7 의 매 dominant data modeling pattern. ## 매 핵심 ### 매 anatomy ```typescript type Result = | { kind: "ok"; value: T } | { kind: "err"; error: E }; // ^^^^^^^^^^^ 매 discriminant — 매 string / number / boolean literal. ``` ### 매 narrowing rules - 매 discriminant 의 literal 의 must. - 매 `switch` / `if` 매 변수 의 narrow. - 매 exhaustive check 의 `never` 의 leverage. ### 매 vs alternatives - **vs class hierarchy**: 매 closed set, 매 pattern-match easy, 매 no `instanceof`. - **vs enum**: 매 enum 의 variant 별 data 의 X — DU 의 carry. - **vs untagged union**: 매 narrow 의 hard, 매 runtime check 의 brittle. ### 매 응용 1. Result / Option / Either monads. 2. State machine state. 3. Redux / xstate action / event. 4. API response variants. 5. AST node types. ## 💻 패턴 ### Pattern 1: Result / Either ```typescript type Result = | { ok: true; value: T } | { ok: false; error: E }; function parseJson(s: string): Result { try { return { ok: true, value: JSON.parse(s) }; } catch (e) { return { ok: false, error: e as Error }; } } const r = parseJson(input); if (r.ok) { r.value.name; // 매 narrowed 의 T } else { r.error.message; // 매 narrowed 의 E } ``` ### Pattern 2: Exhaustive switch with `never` ```typescript type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number } | { kind: "rectangle"; w: number; h: number }; function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; case "square": return s.side ** 2; case "rectangle": return s.w * s.h; default: const _exhaustive: never = s; // 매 새 variant 추가 시 compile error throw new Error(`unhandled: ${_exhaustive}`); } } ``` ### Pattern 3: State machine state ```typescript type FetchState = | { status: "idle" } | { status: "loading"; abortCtrl: AbortController } | { status: "success"; data: T; fetchedAt: Date } | { status: "error"; error: Error; retryCount: number }; function reducer(s: FetchState, ev: FetchEvent): FetchState { if (s.status === "loading" && ev.type === "abort") { s.abortCtrl.abort(); return { status: "idle" }; } // ... } ``` ### Pattern 4: Redux-style action ```typescript type TodoAction = | { type: "ADD"; text: string } | { type: "TOGGLE"; id: string } | { type: "DELETE"; id: string } | { type: "EDIT"; id: string; text: string }; function reducer(state: Todo[], action: TodoAction): Todo[] { switch (action.type) { case "ADD": return [...state, { id: uid(), text: action.text, done: false }]; case "TOGGLE": return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t); case "DELETE": return state.filter(t => t.id !== action.id); case "EDIT": return state.map(t => t.id === action.id ? { ...t, text: action.text } : t); } } ``` ### Pattern 5: API response variants ```typescript type ApiResponse = | { status: 200; data: T } | { status: 401; reason: "expired" | "invalid" } | { status: 429; retryAfterSec: number } | { status: 500; traceId: string }; async function call(url: string): Promise> { /* ... */ } ``` ### Pattern 6: Pattern-match helper (ts-pattern) ```typescript import { match, P } from "ts-pattern"; const message = match(response) .with({ status: 200 }, r => `Got ${r.data.length} items`) .with({ status: 401, reason: "expired" }, () => "Please refresh token") .with({ status: 429 }, r => `Wait ${r.retryAfterSec}s`) .with({ status: 500 }, r => `Error ${r.traceId}`) .exhaustive(); ``` ### Pattern 7: Zod parsing → DU ```typescript import { z } from "zod"; const Event = z.discriminatedUnion("type", [ z.object({ type: z.literal("click"), x: z.number(), y: z.number() }), z.object({ type: z.literal("key"), key: z.string() }), z.object({ type: z.literal("scroll"), dy: z.number() }), ]); type Event = z.infer; ``` ### Pattern 8: assertNever helper ```typescript export function assertNever(x: never): never { throw new Error(`unexpected: ${JSON.stringify(x)}`); } // 매 default case 의 use. ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Closed set of variants | DU | | Open extensible | class + interface | | Boolean flag pair | DU (truthy combos 의 prevent) | | Many variants (~20+) | DU + ts-pattern | | Server boundary | Zod discriminatedUnion | **기본값**: 매 union 의 require 시 매 DU + `kind` / `type` / `status` discriminant. ## 🔗 Graph - 부모: [[TypeScript Type System]] · [[Algebraic Data Types]] - 변형: [[Tagged Unions]] · [[Sum Types]] - 응용: [[Result Type]] · [[State Machine]] · [[Zod]] - Adjacent: [[Pattern Matching]] · [[Exhaustiveness Checking]] ## 🤖 LLM 활용 **언제**: 매 type modeling, 매 state machine, 매 API contract, 매 reducer. **언제 X**: 매 single-variant — 매 plain interface. ## ❌ 안티패턴 - **No discriminant**: 매 untagged union — 매 narrow 의 hard. - **String discriminant 의 typo**: 매 magic string 의 const 의 hoist. - **Boolean flag combos**: `{loading, success, error}` boolean — 매 DU 의 use. - **Default case 의 swallow**: `default: return state` — 매 새 variant 시 silent miss. - **Class hierarchy 의 simulate**: 매 DU 의 cleaner. ## 🧪 검증 / 중복 - Verified (TS handbook "Narrowing", Rust enum docs, F# DU, Zod docs, ts-pattern docs). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — DU patterns + exhaustive + ts-pattern + Zod |