타입스크립트를 사용하다보면 두 가지 타입 중 하나를 선택해야 하는 경우가 종종 있다. 예를 들어 API에서 내려주는 데이터가 아래 두 가지 형태가 있다고 해보자.
{
"data": {
"name": "John",
"age": 30
}
}
{
"error": {
"message": "Not Found"
}
}
이럴 때 Response의 타입을 정의할 때 data
와 error
중 하나만 가질 수 있도록 정의하고 싶다면 어떻게 해야 할까? 유니온 타입을 사용하면 될 것 같다.
type ResponseData = {
data: {
name: string;
age: number;
};
};
type ResponseError = {
error: {
message: string;
};
};
type Res = ResponseData | ResponseError;
하지만 위 코드는 문제가 있다. response는 data를 가지고 있는 경우 error는 가지고 있지 않아야 하고, 반대로 error를 가지고 있는 경우 data는 가지고 있지 않아야 한다. 하지만 위 코드는 data와 error가 동시에 존재할 수 있는 문제가 있다.
// 에러가 발생하지 않는다.
const response: Res = {
data: {
name: 'John',
age: 30,
},
error: {
message: 'Not Found',
},
};
이 문제를 해결하려면 ResponseData
와 ResponseError
를 상호 배타적인 유니온 타입으로 정의해야 한다. 즉, ResponseData
와 ResponseError
가 동시에 존재할 수 없도록 해야 한다.
type ResponseData = {
data: {
name: string;
age: number;
};
error?: never;
};
type ResponseError = {
error: {
message: string;
};
data?: never;
};
type Res = ResponseData | ResponseError;
이렇게 작성하면 ResponseData
와 ResponseError
가 상호 배타적인 유니온 타입이 되어 data
와 error
가 동시에 존재할 수 없게 된다.
// 에러가 발생한다.
const response: Res = {
data: {
name: 'John',
age: 30,
},
error: {
message: 'Not Found',
},
};
XOR 연산자
위 문제를 일반화하면 두 가지 타입 중 하나만 가질 수 있는 상호 배타적인 유니온 타입을 만들어야 한다. 집합적인 관점으로 보면 대칭차집합을 구하는 것과 같다. 이를 위해 XOR 연산자를 정의할 수 있다.
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = T | U extends object
? (Without<T, U> & U) | (Without<U, T> & T)
: T | U;
이를 위 코드에 적용하면 동일한 결과를 얻을 수 있다.
type ResponseData = {
data: {
name: string;
age: number;
};
};
type ResponseError = {
error: {
message: string;
};
};
type Res = XOR<ResponseData, ResponseError>;
이제 Res
는 ResponseData
와 ResponseError
가 상호 배타적인 유니온 타입이 되어 data
와 error
가 동시에 존재할 수 없게 된다.
// 에러가 발생한다.
const response: Res = {
data: {
name: 'John',
age: 30,
},
error: {
message: 'Not Found',
},
};