June 30, 2024

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 연산자를 사용하는 것보다 코드를 더 안전하고 명확하게 작성할 수 있습니다.