[Typescript] Discriminated Union 타입

[Typescript] Discriminated Union 타입
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 핵심 체크리스트

규칙 예시 중요도
모든 멤버에 공통 식별자 존재 typekindstatus 등 ★★★★★
식별자는 리터럴 타입 사용 "success""error" ★★★★★
switch-case와 조합 사용 switch (obj.type) { ... } ★★★★☆
Exhaustiveness Checking 적용 assertNever() 함수 사용 ★★★★☆
직관적인 식별자 이름 선택 stateactioncategory 등 ★★★☆☆

댓글