나쁜 React API 의 절반이 '가끔 이들 같이, 절대 저들은 안 됨' 을 조용히 암시하는 optional prop 에서 와. Discriminated union 이 그 규칙을 명시적으로 만들고 컴파일러가 강제하게 해.
모양
Discriminated union 은 object 타입의 union — 모든 variant 가 단일 literal 필드 (discriminant) 를 다른 값으로 공유. TypeScript 가 discriminant 로 union narrow — 코드에서 discriminant 체크하면 컴파일러가 어떤 variant 인지 앎.
전형적인 Button-or-Link 문제
한 컴포넌트가 onClick 넘기면 스타일 잡힌 button, href 넘기면 스타일 잡힌 anchor 렌더 원함. Optional prop 으로 하면 { onClick?: () => void; href?: string } — 둘 다 넘겨도 (모호) 또는 둘 다 안 넘겨도 (쓸모없음) 컴파일됨. Discriminated-union 버전은 계약을 배타적 으로: 정확히 한 variant 골라야.
컴포넌트 안에서 narrowing
Discriminant 체크하면 (if (props.variant === 'link')) TypeScript 가 타입 narrow — 그 블록 안에선 href 가 required, onClick 은 존재 안 함. 타입 assertion 없음, ! 없음. 컴파일러가 union 을 대신 walk.
스케일이 되는 자리
Button/Link 너머, discriminated union 은 어디나:
- Fetch 결과:
{ status: 'loading' } | { status: 'error'; error: Error } | { status: 'success'; data: T }. - Form state:
{ phase: 'editing'; draft: ... } | { phase: 'submitting' } | { phase: 'saved'; saved: ... }. - Cinder 의 protocol envelope:
{ type: 'preview.captured'; payload: ... } | { type: 'generation.candidate_ready'; payload: ... }— cwkCinder 의packages/protocol이 정확히 쓸 모양.
불가능한 상태를 불가능하게.
{ loading: true, error: 'oops' } 를 허용하는 타입은 결국 그 버그 만들어내는 타입. Discriminated union 이 타입 시스템에 손 뻗어 버그 쓰여지기 전에 제거하는 방식.