Discriminated Union(식별 가능한 유니언 타입) 은 타입스크립트의 타입 추론 시스템을 극대화하는 핵심 기술이다. 복잡한 타입 관계를 단순화하고 런타임 안정성을 확보하는 이 기능을 마스터하면 타입스크립트 코드의 품질을 올릴 수 있다.
1. Discriminated Union이란? 🔍
타입의 정체성을 식별하는 기술
type PaymentMethod =
| { type: "credit"; cardNumber: string }
| { type: "mobile"; phone: string }
| { type: "cash"; currency: "USD" | "KRW" };
- 공통 속성(
type
)으로 각 타입을 식별 - 모든 유니언 멤버가 동일한 리터럴 타입을 가진 속성 보유
일반 유니언과의 차이
// ❌ 일반 유니언 (타입 추론 어려움)
type Shape = { radius: number } | { size: number };
// ✅ Discriminated Union (명확한 식별)
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
2. 왜 꼭 사용해야 할까? 💡
1. 타입 안전성 200% 향상
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // radius 접근 가능
case "square":
return shape.size ** 2; // size 접근 가능
}
}
2. 리팩토링 시 자동 감지
// 새로운 타입 추가 시
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "triangle"; base: number; height: number }; // 추가
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle": /* ... */
case "square": /* ... */
// ❌ 'triangle' 케이스 처리하지 않으면 컴파일 에러
}
}
3. 실전 패턴 🔥
1. API 응답 처리
type ApiResponse<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; message: string };
function handleResponse(response: ApiResponse<User>) {
switch (response.status) {
case "loading":
return <Spinner />;
case "success":
return <Profile data={response.data} />; // data 접근
case "error":
return <ErrorMessage msg={response.message} />;
}
}
2. 상태 머신 구현
type TrafficLight =
| { state: "red"; next: "green" }
| { state: "yellow"; next: "red" }
| { state: "green"; next: "yellow" };
function changeLight(current: TrafficLight): TrafficLight {
return { state: current.next } as TrafficLight;
}
3. 이벤트 핸들링
type AppEvent =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "scroll"; delta: number };
function handleEvent(event: AppEvent) {
switch (event.type) {
case "click":
console.log(`Clicked at (${event.x}, ${event.y})`);
break;
case "keypress":
console.log(`Pressed key: ${event.key}`);
break;
case "scroll":
console.log(`Scrolled by ${event.delta}px`);
break;
}
}
4. Exhaustiveness Checking (완전성 검사)
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle": /* ... */
case "square": /* ... */
default:
return assertNever(shape); // ❌ 새로운 타입 추가 시 여기서 에러
}
}
4. 주의해야 할 함정 🚨
1. 식별자 속성 이름 통일
// ❌ Bad (식별자 속성 이름 불일치)
type InvalidUnion =
| { kind: "circle"; radius: number }
| { type: "square"; size: number }; // 'kind' vs 'type'
// ✅ Good
type ValidUnion =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
2. 리터럴 타입 사용 필수
// ❌ Bad (식별자 타입이 일반 string)
type PaymentMethod =
| { type: string; cardNumber: string }
| { type: string; phone: string };
// ✅ Good
type PaymentMethod =
| { type: "credit"; cardNumber: string }
| { type: "mobile"; phone: string };
5. 고급 활용 테크닉 ⚡
1. 계층적 구조 설계
type Vehicle =
| { category: "land"; wheels: number }
| { category: "water"; buoyancy: number };
type LandVehicle = Vehicle & { category: "land" } & (
| { type: "car"; seats: number }
| { type: "truck"; payload: number }
);
2. 템플릿 리터럴 타입 조합
type EventType = "click" | "hover" | "drag";
type ComponentEvent =
| { type: `${EventType}-start`; timestamp: number }
| { type: `${EventType}-end`; duration: number };
const event: ComponentEvent = {
type: "drag-end", // 자동 완성 지원
duration: 1500
};
📌 Discriminated Union 핵심 체크리스트
규칙 |
예시 |
중요도 |
모든 멤버에 공통 식별자 존재 |
type , kind , status 등 |
★★★★★ |
식별자는 리터럴 타입 사용 |
"success" , "error" |
★★★★★ |
switch-case와 조합 사용 |
switch (obj.type) { ... } |
★★★★☆ |
Exhaustiveness Checking 적용 |
assertNever() 함수 사용 |
★★★★☆ |
직관적인 식별자 이름 선택 |
state , action , category 등 |
★★★☆☆ |
댓글