Typescript Discriminated Union Type이란?

silver·2025년 6월 4일

타입스크립트

목록 보기
2/3
post-thumbnail

Discriminated Union Type이란?

Discriminated Union Type은 공통된 리터럴 속성(값이 고정된 속성)을 가진 여러 객체 타입을 하나의 유니온 타입으로 묶은 것이다. 공통 속성을 통해 타입을 구분(Discriminate)할 수 있어서 조건문 등을 통해 안전하게 타입을 좁힐 수 있다.


interface Dog {
  type : 'dog';
  bark : ()=>void;
 }

interface Cat {
  type : 'cat';
  meow : ()=>void;
}

type Pet = Dog | Cat;

const handlePet =(pet:Pet)=> {
  switch(pet.type) {
    case 'dog':
      pet.bark();
      break;
    case 'cat':
      pet.meow();
      break;
  }

const dog = {
  type: 'dog',
  bark : ()=> console.log('왈왈'),
}

const cat = {
  type: 'cat',
  meow : ()=> console.log('야옹'),
}

handlePet(dog); // '왈왈'
handlePet(cat); // '야옹'

위 예시처럼 type이라는 공통된 리터럴 속성을 갖고 있다면 handlePet함수 안에서 pet.type값을 기준으로 타입을 정확하게 좁힐 수 있다.

또한 case 'dog' 블록 안에서는 pet.을 입력하면 자동으로 bark()와 같이 dog타입의 속성만 자동완성으로 표시되기 때문에 생산성을 높일 수 있다.

in 타입 가드의 한계

만약 type이라는 공통된 리터럴 속성이 없다면 in연산자를 사용해 타입을 좁혀야 한다.


const handlePet=(pet:Pet)=> {
  if ('bark' in pet) return pet.bark();
  if ('meow' in pet) return pet.meow();
}

이러한 방식도 동작은 하지만 다음과 같은 단점이 있다.

1.속성이 많아질수록 in을 통한 조건문이 많아진다.

만약 속성이 'bark'와 'meow' 뿐이라면 이렇게 해도 큰 문제는 없지만 더 많은 속성들이 타입 안에 있을 경우 속성들을 사용할 때 하나하나 in을 통해 확인해야한다.

2.중복된 속성이 될 경우

속성이 추가되더라도 만약 'bark'dog에 유일하게 있는 속성이라면 'bark' in pet 을 통해 타입을 dog로 좁힐 수 있지만, 만약 wolf라는 타입이 추가돼서 bark속성을 갖게 된다면 dog로 타입을 좁히는 것이 불가능해진다.

3.자동완성 미지원

또한 'bark' in pet과 같이 타입을 검증할 땐 자동완성이 뜨지 않는다는 점과 만약 bark 함수의 이름이 변경되더라도 'bark' in pet이 컴파일 에러 없이 그대로 남아서 버그를 유발할 수 있다.

Exhaustiveness Checking을 통한 안전한 타입 처리

Discriminated Union타입을 Switch문으로 좁힐 때 모든 case를 처리하지 않는다면 타입이 누락되는 상황이 발생할 수 있다.
이를 방지하기 위해 never 타입과 default 블록을 활용하면 타입이 누락될 경우 컴파일 타임에 오류를 발생시켜서 타입 누락을 막을 수 있다.


interface Dog {
  type : 'dog';
  bark : ()=>void;
}

interface Cat {
  type : 'cat';
  meow : ()=>void;
}

interface Bird {
  type : 'bird';
  chirp : ()=>void;
}

type Pet = Dog | Cat | Bird;

const handlePet =(pet:Pet)=> {
  switch (pet.type) {
    case 'dog':
      pet.bark();
      break;
    case 'cat':
      pet.meow();
      break;
    case 'bird':
      pet.chirp();
      break;
    default:
      const _exhaustiveCheck: never = pet;
      return _exhaustiveCheck;
  }
}

여기서 만약 새로운 타입인 TurtlePet 타입에 추가되거나 bird타입이 빠지게 되면 컴파일 에러가 표시된다.

모든 타입에 대한 case가 있을 때

Turtle이 추가됐을 때

Bird가 빠졌을 때

Turtle이 추가됐을 떈 pet.type의 default로 never가 아닌 Turtle이 와서 에러가 발생하고
Bird가 빠졌을 땐 Pet의 타입인 "dog" | "cat"과 일치하지 않는다는 오류가 표시되는 것을 볼 수 있다.

실제 사용 사례

위와 같이 서로 공통된 요소를 갖고 있지만 조금씩 다른 요소들이 존재하는 UI를 하나의 컴포넌트로 구현하기 위해 Discriminated Union 타입을 사용했다.


// 공통된 props(기사님 이름, 리뷰 평점, 리뷰 수, 경력 등등)
interface BaseProps {
  movingType : MovingType[];
  moverName : string;
  rating : number;
  //... 그 외 공통된 속성들
}

interface CompletedQuoteProps extends BaseProps {
  variant: 'quote';
  subVariant: 'completed';
  description?: string;
  price?: number;
}

interface PendingQuoteProps extends BaseProps {
  variant : 'quote';
  subVariant : 'pending';
  quoteId: string;
  price? : number;
  // ... 그 외 속성들
}

interface WrittenReviewProps extends BaseProps {
  variant : 'review';
  subVariant : 'written';
  reviewContent : string;
  writtenAt : Date;
}

interface PendingReviewProps extends BaseProps {
  variant : 'review';
  subVariant : 'pending';
  onClickReviewButton : ()=>void;
  // ... 그 외 속성들
}

type MoverInfoProps = CompletedQuoteProps | PendingQuoteProps 
| WrittenReviewProps | PendingReviewProps;

먼저 공통으로 사용되는 props들을 BaseProps로 선언하고 모든 유형의 interface에서 extends를 통해 해당 interface를 확장하도록 했다.

그리고 variant속성을 discriminant(구분자)로 사용해 유형을 구분했다. 큰 구분자 안에서 세부 구분을 위해 subVariant를 추가했는데 이렇게 할 경우 variantquote일 땐 subVariantcompletedpending중 선택할 수 있게 좁혀진다.


export default function MoverInfo(props : MoverInfoProps) {
	return (
      <div>
        <공통된UI {...someProps} />
        {props.variant === 'quote' && (
          { props.subVariant === 'completed' && (
           <CompletedQuote에 해당하는 UI/>
           )}
        {props.variant === 'review' && (
          { props.subvariant === 'written' && (
           <WrittenReview에 해당하는 UI/>
           )}
        // ... 그 외 UI
      </div>
    )
}

이렇게 컴포넌트 내에서 props의 variant,subVariant를 통해 표시할 UI를 선택할 수 있다.

0개의 댓글