[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] 타입 확장하기  (0) 2025.01.23
[TS] 제네릭 사용법  (0) 2025.01.22