[TS] 식별할 수 있는 유니온, Exhaustiveness Checking

식별할 수 있는 유니온(Discriminated Unions)

  • 태그된 유니온으로도 불리는 식별할 수 있는 유니온은 타입 좁히기에 널리 사용되는 방식이다. 식별할 수 있는 유니온이란 타입 간의 구조 호환을 막기 위해 타입마다 구분할 수 있는 판별자를 달아 포함 관계를 제거하는 것이다.
  • 타입스크립트는 구조적 타입 시스템(덕 타이핑) 언어이기 때문에 객체로 이루어진 유니온 타입의 하위 타입에 대해 타입 에러를 발생시키지 않는다. 따라서 판별자를 통해 타입을 구분해야 한다.
type TextError = {
  errorCode: string;
  errorMessage: string;
}

type ToastError = {
  errorCode: string;
  errorMessage: string;
  toastShowDuration: number;
}

type AlertError = {
  errorCode: string;
  errorMessage: string;
  onConfirm: () => void;
}

type ErrorFeedbackType = TextError | ToastError | AlertError;

const errorArr: ErrorFeedbackType[] = [
  { errorCode: '100', errorMessage: '텍스트 에러' },
  { errorCode: '200', errorMessage: '토스트 에러', toastShowDuration: 3000 },
  { errorCode: '300', errorMessage: '얼럿 에러', onConfirm: () => {} },
  // 타입 에러가 발생하지 않는다.
  { errorCode: '999', errorMessage: '잘못된 에러', toastShowDuration: 3000, onConfirm: () => {} },
];
  • 서로 호환되지 않도록 만들어주기 위해서는 타입들이 서로 포함 관계를 가지지 않도록 정의해야 한다. 이 때 적용할 수 있는 방식이 식별할 수 있는 유니온이다. 판별자의 개념으로 errorType 이라는 필드를 새로 정의한다.
type TextError = {
  errorType: 'TEXT';
  errorCode: string;
  errorMessage: string;
}

type ToastError = {
  errorType: 'TOAST';
  errorCode: string;
  errorMessage: string;
  toastShowDuration: number;
}

type AlertError = {
  errorType: 'ALERT';
  errorCode: string;
  errorMessage: string;
  onConfirm: () => void;
}

type ErrorFeedbackType = TextError | ToastError | AlertError;

const errorArr: ErrorFeedbackType[] = [
  { errorType: 'TEXT', errorCode: '100', errorMessage: '텍스트 에러' },
  { errorType: 'TOAST', errorCode: '200', errorMessage: '토스트 에러', toastShowDuration: 3000 },
  { errorType: 'ALERT', errorCode: '300', errorMessage: '얼럿 에러', onConfirm: () => {} },
  // 타입 에러가 발생한다.
  { errorType: 'ALERT', errorCode: '999', errorMessage: '잘못된 에러', toastShowDuration: 3000, onConfirm: () => {} },
];
  • 식별할 수 있는 유니온의 판별자는 유닛 타입으로 선언되어야 정상적으로 동작한다. 유닛 타입은 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입을 말한다.
  • 판별자가 value 이면 정상적으로 동작하지 않지만 answer 이면 정상적으로 동작한다.
interface A {
  value: 'a'; // 유닛 타입
  answer: 1;
}

interface B {
  value: string; // not 유닛 타입
  answer: 2;
}

interface C {
  value: Error; // 인스턴스화 가능한 타입(not 유닛 타입)
  answer: 3;
}

type Union = A | B | C;
function handle(param: Union) {
  param.answer // 1 | 2 | 3

  if (param.value === 'a') {
    param.answer // 1 | 2
  }

  if (typeof param.value === 'string') {
    param.answer // 1 | 2
  }

  if (param.value instanceof Error) {
    param.answer // 1 | 2 | 3
  }

  /** 판별자가 answer일 때 */
  param.value // string | Error
}

Exhaustiveness Checking

  • Exhaustiveness는 사전적으로 철저함, 완전함을 의미한다. Exhaustiveness Checking은 모든 케이스에 대해 철저하게 타입을 검사하는 것을 말하며 타입 좁히기에 사용되는 패러다임 중 하나이다.
  • exhaustiveCheck라는 함수는 매개변수를 never 타입으로 선언하고 있다. 따라서 값이 들어오면 에러를 발생시킨다. 이 함수를 타입 처리 조건문의 마지막 else 문에 사용하면 앞의 조건문에서 모든 타입에 대한 분기 처리를 강제할 수 있다.
  • “5000” 이라는 새로운 타입이 추가되었는데 분기 처리를 하지 않았기 때문에 에러가 발생한다.
type ProductPrice = '10000' | '20000' | '5000';

const getProductName = (productPrice: ProductPrice): string => {
  if (productPrice === '10000') return '상품권 1만 원';
  if (productPrice === '20000') return '상품권 2만 원';
  // if (productPrice === '5000') return '상품권 5천 원'; 새로운 조건 추가
  else {
    exhaustiveCheck(productPrice) // never인데 값이 있어 타입 에러 발생
    return '상품권';
  }
}

const exhaustiveCheck = (param: never) => {
  throw new Error('type error!');
}
  • 이렇게 Exhaustiveness Checking을 활용하면 예상치 못한 런타임 에러를 방지하거나 요구사항이 변경되었을 때 생길 수 있는 위험성을 줄일 수 있다.

'TypeScript' 카테고리의 다른 글

[TS] 유틸리티 타입  (0) 2025.01.27
[TS] 조건부 타입  (0) 2025.01.26
[TS] 타입 가드  (1) 2025.01.24
[TS] 타입 확장하기  (1) 2025.01.23
[TS] 제네릭 사용법  (0) 2025.01.22