🔀

판별 유니언 — TypeScript에서 enum 대신 쓰는 패턴

type 필드로 분기하면 TypeScript가 각 분기의 타입을 자동으로 좁혀준다

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':     return Math.PI * shape.radius ** 2;    // shape.radius 접근 가능
    case 'rectangle':  return shape.width * shape.height;      // shape.width 접근 가능
    case 'triangle':   return 0.5 * shape.base * shape.height;
  }
}

shape.kind로 분기하면 TypeScript가 각 case 안에서 shape의 타입을 자동으로 좁혀준다. circle case에서는 shape.radius에 접근할 수 있고, rectangle case에서는 shape.width에 접근할 수 있다.

왜 enum 대신 쓰는가

enum은 런타임에 코드가 생성된다 (역방향 매핑 객체). 판별 유니언은 타입 레벨에서만 존재하고 런타임 코드가 0이다.

// enum — 런타임에 객체가 생성됨
enum Direction { Up, Down }  // Direction[0] === 'Up' 역매핑

// 판별 유니언 — 런타임 비용 0
type Direction = 'up' | 'down';

exhaustive check

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':    return ...;
    case 'rectangle': return ...;
    // 'triangle' 빠뜨리면?
    default:
      const _exhaustive: never = shape;  // 컴파일 에러! triangle이 never에 안 맞음
      return _exhaustive;
  }
}

never 타입으로 모든 케이스를 처리했는지 컴파일 타임에 검증할 수 있다. Rust의 exhaustive match와 같은 효과.

핵심 포인트

1

유니언의 각 타입에 공통 리터럴 필드(kind, type 등)를 둔다 = discriminant

2

switch/if로 discriminant를 분기하면 TypeScript가 자동 타입 좁히기

3

never 타입으로 exhaustive check — 케이스 누락 시 컴파일 에러

4

enum보다 런타임 비용 0 + 타입 안전성 동일 (또는 더 좋음)

사용 사례

Redux action — { type: "INCREMENT" } | { type: "SET_VALUE", payload: number } API 응답 — { status: "success", data: T } | { status: "error", message: string }