Typescript 태그드 유니온(Tagged Unions)

Typescript에서 타입을 지정할 때, 같은 카테고리에 속하지만 세부 항목이 다른 경우가 있습니다. 예를 들어 결제 서비스를 구축한다고 가정해봅시다. 결제 방식으로는 카드 결제와 계좌 이체 두 가지를 생각해보겠습니다. 카드 결제의 경우 카드 번호(creditCardNumber)와 만료일(expirationDate)이 필요하고, 계좌 이체의 경우 은행명(bankName)과 계좌 번호(accountNumber)가 필요합니다. 이를 타입으로 지정하면 다음과 같습니다:

type CreditCardPayment = {
  creditCardNumber: string;
  expirationDate: string;
};

type BankTransferPayment = {
  bankName: string;
  accountNumber: string;
};

그리고 결제 함수를 작성해봅시다:

function pay(payment: CreditCardPayment | BankTransferPayment) {
  if ('creditCardNumber' in payment) {
    console.log('카드 결제');
    payment.creditCardNumber; // 타입 추론을 통해 creditCardNumber 사용 가능
  } else {
    console.log('계좌 이체');
    payment.bankName; // 타입 추론을 통해 bankName 사용 가능
  }
}

Typescript Playground 바로가기

여기서 payment가 어떤 타입인지 확인하기 위해 in 연산자를 사용했습니다. in 연산자는 Javascript의 Built-in 연산자로, 객체가 특정 프로퍼티를 가지고 있는지 확인하는 데 사용하는 연산자입니다. 이를 활용하여 paymentcreditCardNumber 프로퍼티를 가지고 있으면 CreditCardPayment로, 그렇지 않으면 BankTransferPayment로 구분하고 있습니다.

하지만 이 방식에는 몇 가지 문제가 있습니다. 예를 들어, 각 타입의 정의가 변경될 때마다 코드 수정이 필요할 수 있고, 두 타입 간에 겹치는 프로퍼티가 있을 경우 문제가 발생할 수 있습니다. 만약 CreditCardPaymentBankTransferPaymentnumber라는 동일한 프로퍼티가 있다면 어떻게 될까요? 다음과 같은 상황을 생각해봅시다:

type CreditCardPayment = {
  number: string;
  expirationDate: string;
};
type BankTransferPayment = {
  number: string;
  bankName: string;
};

function pay(payment: CreditCardPayment | BankTransferPayment) {
  if ('number' in payment) { // number로 타입 구분 불가능
    console.log('카드 결제');
  } else {
    console.log('계좌 이체');
  }
}

이 경우, number라는 프로퍼티로는 결제 방식을 구분할 수 없습니다.

Typescript Playground 바로가기

이러한 문제를 해결하기 위해 Typescript에서는 태그드 유니온 타입(Tagged Unions Type) 또는 판별 유니온(Discriminated Unions Type)을 사용할 수 있습니다. 태그드 유니온 타입은 타입 구분을 위한 프로퍼티를 추가하여 이를 통해 타입을 구분하는 방식입니다. 위의 예제를 태그드 유니온 타입으로 수정하면 다음과 같습니다:

type CreditCardPayment = {
  method: 'creditCard';
  creditCardNumber: string;
  expirationDate: string;
};

type BankTransferPayment = {
  method: 'bankTransfer';
  bankName: string;
  accountNumber: string;
};

각 타입에 method라는 프로퍼티를 추가하고, 이 프로퍼티를 사용하여 타입을 구분합니다. 결제 함수도 다음과 같이 수정합니다:

function pay(payment: CreditCardPayment | BankTransferPayment) {
  if (payment.method === 'creditCard') {
    console.log('카드 결제');
    payment.creditCardNumber; // 타입 추론을 통해 creditCardNumber 사용 가능
  } else {
    console.log('계좌 이체');
    payment.bankName; // 타입 추론을 통해 bankName 사용 가능
  }
}

이제 각 타입의 정의가 변경되더라도 method 프로퍼티만 유지하면 결제 함수에서 타입을 구분하는 코드를 수정할 필요가 없습니다. 또한, 두 타입에 동일한 이름의 프로퍼티가 존재하더라도 method 프로퍼티를 기준으로 정확하게 타입을 구분할 수 있습니다. 이처럼 태그드 유니온 타입을 사용하면 in 연산자를 사용하는 것보다 코드를 더 안전하고 명확하게 작성할 수 있습니다.