TypeScript에서 Union Type과 Discriminated Union은 강력한 타입 안전성을 유지하면서도 다양한 상황에 유연하게 대처할 수 있는 방법이다.
이 패턴은 특히 복잡한 객체 구조나 상태 관리를 처리할 때 유용하다.
이 글에서는 Union Type과 Discriminated Union에 대해 설명하고, 이를 어떻게 활용할 수 있는지 자세히 알아본다.
Union Type은 TypeScript에서 두 개 이상의 타입을 결합하여 하나의 타입으로 정의할 수 있는 기능이다.
이를 통해 변수나 함수의 매개변수가 여러 타입을 가질 수 있도록 선언할 수 있다. Union Type은 | (파이프) 기호를 사용하여 정의된다.
type StringOrNumber = string | number;
let value: StringOrNumber;
value = "Hello"; // 유효하다
value = 42; // 유효하다
value = true; // 오류: 'boolean' 타입은 'StringOrNumber' 타입에 할당할 수 없다.
위의 예제에서 value는 string 또는 number 타입만 가질 수 있다.
boolean 값을 할당하려고 하면 TypeScript 컴파일러가 오류를 발생시킨다.
Discriminated Union은 Union Type을 한 단계 발전시킨 개념으로, 공통된 식별자(discriminant)를 포함하는 여러 객체 타입을 결합하여 타입 안전성을 강화한다.
이를 사용하면 TypeScript 컴파일러가 특정 객체의 타입을 자동으로 추론할 수 있어 코드의 가독성과 안전성을 높일 수 있다.
예를 들어, 여러 종류의 알림을 처리하는 함수가 있다고 가정한다.
알림은 텍스트 메시지일 수도 있고, 이메일일 수도 있으며, 푸시 알림일 수도 있다.
각 알림은 고유의 속성을 가지고 있지만, 공통적으로 type 속성을 통해 구분된다.
// 1. 알림 타입 정의
type TextNotification = {
type: "text";
message: string;
sender: string;
};
type EmailNotification = {
type: "email";
subject: string;
body: string;
sender: string;
recipient: string;
};
type PushNotification = {
type: "push";
title: string;
message: string;
sender: string;
deviceToken: string;
};
// 2. Discriminated Union으로 알림 타입 정의
type Notification = TextNotification | EmailNotification | PushNotification;
여기서 Notification 타입은 TextNotification, EmailNotification, PushNotification 세 가지 타입을 포함하는 Union Type이다.
각 타입은 type 속성을 통해 서로 다른 종류의 알림임을 구분할 수 있다.
이제 Notification 타입을 처리하는 함수를 작성해 본다.
type 속성을 기준으로 알림의 타입을 구분하고, 각 타입에 맞는 처리를 수행한다.
function handleNotification(notification: Notification) {
switch (notification.type) {
case "text":
console.log(`Text from ${notification.sender}: ${notification.message}`);
break;
case "email":
console.log(`Email to ${notification.recipient} from ${notification.sender}`);
console.log(`Subject: ${notification.subject}`);
console.log(`Body: ${notification.body}`);
break;
case "push":
console.log(`Push Notification: ${notification.title}`);
console.log(`Message: ${notification.message}`);
console.log(`Sent to device: ${notification.deviceToken}`);
break;
default:
console.error("Unknown notification type");
}
}
이 함수는 notification.type을 기반으로 타입을 안전하게 구분하고, 각 타입에 맞는 동작을 수행한다.
TypeScript는 switch 문을 통해 notification 객체의 타입을 좁히기 때문에, 각 분기 내에서 해당 타입에 속하는 속성에만 접근할 수 있다.
타입 안전성: TypeScript가 각 타입을 정확히 추론할 수 있어, 잘못된 타입의 접근을 방지할 수 있다.
예를 들어, TextNotification에서 recipient 속성을 참조하려고 하면 컴파일러가 오류를 발생시킨다.
가독성: 코드가 명확하게 분기되며, 각 타입에 맞는 처리가 한눈에 들어온다.
유연성: 새로운 알림 타입이 추가될 경우, 기존 코드를 크게 변경하지 않고도 패턴에 따라 쉽게 확장할 수 있다.
이 패턴은 Redux와 같은 상태 관리 라이브러리에서 다양한 액션 타입을 처리할 때 매우 유용하다.
Redux 액션은 type 속성을 기반으로 구분되며, Discriminated Union을 적용하면 각 액션 타입에 맞는 리듀서를 안전하게 작성할 수 있다.
// Redux 액션 타입 정의
type IncrementAction = {
type: "INCREMENT";
amount: number;
};
type DecrementAction = {
type: "DECREMENT";
amount: number;
};
// Discriminated Union으로 액션 타입 정의
type CounterAction = IncrementAction | DecrementAction;
// 리듀서 작성
function counterReducer(state: number, action: CounterAction): number {
switch (action.type) {
case "INCREMENT":
return state + action.amount;
case "DECREMENT":
return state - action.amount;
default:
return state;
}
}
이 예제에서 CounterAction 타입은 IncrementAction과 DecrementAction을 포함하는 Union Type이다.
리듀서 함수는 action.type에 따라 동작을 구분하며, TypeScript는 각 분기에서 해당 타입의 속성에만 접근할 수 있도록 한다.
TypeScript의 Union Type과 Discriminated Union은 타입 안전성을 유지하면서 유연한 코드를 작성할 수 있게 해준다.
이 패턴은 특히 다양한 타입을 처리하거나 복잡한 상태를 관리해야 하는 경우에 매우 유용하다.
이를 통해 코드의 가독성과 유지보수성을 높일 수 있으며, 컴파일러가 타입 오류를 미리 감지할 수 있어 안전한 코드를 작성하는 데 큰 도움이 된다.