TypeScript기본 타입과 좁히기

discriminated union과 exhaustiveness

분기마다 다른 형태를 갖는 데이터를 discriminated union으로 모델링하고, switch 분기를 빠짐없이 검사하는 방법을 정리합니다.

마지막 수정 2026년 3월 22일

기본 패턴

ts
type Loading = { state: "loading" };
type Success = { state: "success"; data: string[] };
type Failure = { state: "failure"; message: string };

type Result = Loading | Success | Failure;

function render(result: Result) {
  switch (result.state) {
    case "loading":
      return "loading...";
    case "success":
      return result.data.join(", ");
    case "failure":
      return result.message;
    default: {
      const _exhaustive: never = result;
      return _exhaustive;
    }
  }
}

설명

  • discriminated union은 여러 객체 타입이 공통 태그 필드 하나를 공유하도록 설계한 union입니다. state, kind, type 같은 필드가 분기 기준이 됩니다.
  • 장점은 TypeScript가 분기문 안에서 태그 값을 보고 자동으로 타입을 좁힐 수 있다는 점입니다. 그래서 ifswitch가 단순 제어 흐름이 아니라 타입 안전한 모델링 도구가 됩니다.
  • 이 패턴은 네트워크 상태, 폼 상태, 비동기 결과, reducer action처럼 "서로 다른 경우의 수"가 분명한 데이터를 표현할 때 특히 강합니다.
  • never를 이용한 exhaustiveness check는 새 케이스를 나중에 추가했을 때 기존 분기문이 빠짐없이 수정되도록 도와줍니다. 즉 런타임 버그를 컴파일 단계로 끌어올리는 장치입니다.
  • union이 커질수록 중요한 것은 문법보다 도메인 모델링입니다. 태그 이름을 일관되게 유지하고, 각 분기가 가져야 할 데이터만 분명하게 넣는 설계가 더 중요합니다.

빠른 정리

요소역할
태그 필드분기 기준
union가능한 상태 집합
narrowing분기 안에서 타입 자동 축소
never빠진 분기를 컴파일러가 잡게 함
잘 맞는 곳async 상태, action, 화면 상태

주의할 점

태그 필드 없이 union만 나열하면 분기할 때마다 수동 타입 단언이 늘어나기 쉽습니다. 분기가 중요한 데이터라면 처음부터 discriminated union으로 설계하는 편이 훨씬 안정적입니다.

참고 링크

2 sources