optional prop만 늘어놓으면 허용되는 조합과 금지되는 조합이 흐려져. discriminated union으로 그 규칙을 타입에 새겨 보자.
기본 모양
Discriminated union은 여러 객체 타입이 variant, kind, status 같은 공통 필드를 서로 다른 리터럴 값으로 공유하는 union이야. TypeScript는 그 필드를 검사한 뒤 현재 객체가 어느 타입인지 좁혀 줘.
Button과 Link를 하나의 계약으로
하나의 컴포넌트가 button 또는 link가 되어야 한다고 해 보자. 모든 prop을 optional로 만들면 href와 onClick을 둘 다 넘기거나 둘 다 빼도 컴파일돼. 대신 variant: 'button'에는 onClick을, variant: 'link'에는 href를 요구하는 두 타입을 union으로 묶으면 정확히 한 모양만 고를 수 있어.
컴포넌트 안에서 narrowing하기
props.variant === 'link'를 확인한 블록에서는 href가 필수이고 onClick은 없는 타입으로 좁혀져. 타입 assertion 없이도 컴파일러가 올바른 prop을 알려 줘.
어디에 쓰나
이 패턴은 UI variant뿐 아니라 fetch의 loading·error·success 상태에도 잘 맞아. Form의 idle·submitting·success·error와 cwkCinder의 request·response·오류 규약 봉투도 같은 방식으로 표현할 수 있어.
불가능한 prop 조합을 타입에서 없애
Button이 link와 button 두 역할을 맡는다면 variant: 'link'일 때는 href를 요구하고 onClick 전용 계약은 막을 수 있어. variant: 'button'일 때는 type과 onClick을 허용하고 href는 거부해. 모든 필드를 optional로 둔 한 객체보다 호출 지점의 실수를 훨씬 일찍 잡아.
Switch의 빠진 분기를 검사해
각 case에서 discriminant를 확인하면 TypeScript가 해당 variant의 필드만 남겨 줘. 마지막 default에서 값을 never에 할당하면 새 variant를 추가하고 렌더링 분기를 잊었을 때 컴파일 오류가 나.
UI 밖의 상태에도 같은 구조를 써
Fetch는 loading·error·success, form은 idle·submitting·success·error로 표현할 수 있어. cwkCinder의 request·response·오류 규약 봉투처럼 단계마다 필요한 데이터가 다른 메시지 계약에도 잘 맞아.