식별할 수 있는 유니온(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; |
| answer: 2; |
| } |
| |
| interface C { |
| value: Error; |
| answer: 3; |
| } |
| |
| type Union = A | B | C; |
| function handle(param: Union) { |
| param.answer |
| |
| if (param.value === 'a') { |
| param.answer |
| } |
| |
| if (typeof param.value === 'string') { |
| param.answer |
| } |
| |
| if (param.value instanceof Error) { |
| param.answer |
| } |
| |
| |
| param.value |
| } |
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만 원'; |
| |
| else { |
| exhaustiveCheck(productPrice) |
| return '상품권'; |
| } |
| } |
| |
| const exhaustiveCheck = (param: never) => { |
| throw new Error('type error!'); |
| } |
- 이렇게 Exhaustiveness Checking을 활용하면 예상치 못한 런타임 에러를 방지하거나 요구사항이 변경되었을 때 생길 수 있는 위험성을 줄일 수 있다.