Discriminated Union: Typescript에서 타입을 유연하게 확장하는 방법

728x90

시작하며

car와 motorbike를 포함하는 타입 'vehicle'

타입스크립트를 사용하다 보면 하나의 타입에 여러 가지 객체가 추가되며, 특정 경우를 제외하고는 항상 undefined 이거나 값이 존재하는 경우가 있습니다. 예를 들어, 아래와 같이 Vehicle이라는 타입이 있을 때 해당 타입으로 차나 오토바이를 만들 수 있다고 가정해 보겠습니다.

type Vehicle = {
  type: 'motorbike' | 'car';
  make: string;
  model: string;
  fuel: 'petrol' | 'diesel';
  doors?: number;
  bootSize?: number;
};

하지만 위 코드로는 doors가 3개인 오토바이와 같이 실제로 존재할 수 없는 객체를 만들 수도 있습니다. 이때 오토바이라면 doors나 bootSize와 같은 프로퍼티는 할당할 수 없도록 작업할 수 있는 기능이 바로 Discriminated Union입니다. Discriminated Union을 사용하면 여러 가지 타입들을 하나의 타입으로 표현할 수 있게 해 줍니다. 이 글에서는 Discriminated Union이 무엇인지, 왜 중요한지, 그리고 어떻게 사용하는지에 대해 알아보겠습니다.

Discriminated Union이란?

Discriminated Union은 TypeScript의 기능으로, 하나의 타입에서 가능한 여러 경우의 수를 식별자를 기준으로 분리합니다. 각 경우에 식별자를 부여함으로써, TypeScript의 타입 시스템은 모든 가능한 경우를 적절하게 처리할 수 있도록 도와줍니다. 식별자는 문자열 리터럴, 숫자 리터럴 또는 심벌일 수 있습니다. 말이 어렵게 느껴질 수 있으니, 예제 코드를 통해 함께 살펴보겠습니다.

예제 코드

우선 서론에서 봤던 Vehicle이라는 타입에 Discriminated Union을 적용해 보겠습니다.

// 기존 Vehicle 타입
type Vehicle = {
  type: 'motorbike' | 'car';
  make: string;
  model: string;
  fuel: 'petrol' | 'diesel';
  doors?: number;
  bootSize?: number;
};

// Discriminated Union을 적용한 Vehicle 타입
type VehicleBase = {
  make: string;
  model: string;
  fuel: 'petrol' | 'diesel';
};

type Car = VehicleBase & {
  type: 'car';
  doors: number;
  bootSize: number;
};

type Motorbike = VehicleBase & {
  type: 'motorbike';
  fuel: 'petrol';
};

type Vehicle = Car | Motorbike;

 

차이가 느껴지시나요? 기존 Vehicle 타입에서는 Car에서만 사용하는 속성과 Motorbike에서 사용하는 속성이 섞여 있었습니다. 하지만 Discriminated Union을 적용한 코드에서는 type이라는 식별자를 통해 type이 car일 때는 doors와 bootSize 속성을, type이 motorbike일 때는 fuel 속성만 가지도록 분리되었습니다. 이렇게 하면 각 타입의 역할이 명확해지고, 코드의 가독성과 유지보수성이 크게 향상됩니다.

Discriminated Union의 장점

Discriminated Union의 장점은, 타입 안전성을 높여준다는 점입니다. 예를 들어, 차량을 처리하는 함수를 작성할 때 다음과 같이 할 수 있습니다.

   const vehicleHandler = (vehicle: Vehicle) => {
      switch (vehicle.type) {
        case 'car':
          console.log(`The car has ${vehicle.doors} doors`);
          break;
        case 'motorbike':
          console.log(`The only fuel my motorbike can take is ${vehicle.fuel}`);
          break;
      }
    };

 

이 switch 문이나 if 조건과 같은 조건 블록을 사용하면, IDE는 해당 블록 내에서 사용할 수 있는 속성만 표시합니다. 예를 들어, vehicle.type이 'car'인 경우에는 doors와 bootSize 속성에 접근할 수 있고, vehicle.type이 'motorbike'인 경우에는 fuel 속성에 접근할 수 있습니다. 즉, 해당 Type Guard 내에서 없는 속성에 접근할 수 없게 되는 거죠. 이렇게 누락되거나 일치하지 않는 식별자를 잡아내어 버그의 가능성을 줄이고, 정확한 타입을 제공하기 때문에 타입 안정성을 높일 수 있습니다.

 

결론

Discriminated Union은 TypeScript에서 타입 안전성을 높이고 코드의 가독성과 유지보수성을 향상하는 강력한 도구입니다. 이를 통해 각 타입의 역할을 명확하게 정의하고, 불필요한 속성의 혼재를 방지할 수 있습니다. 또한, 조건문을 통해 각 타입에 맞는 속성만을 안전하게 접근할 수 있게 되어, 개발 중 발생할 수 있는 오류를 줄이고, 코드의 신뢰성을 높일 수 있습니다. 따라서, 복잡한 타입 구조를 다루는 프로젝트에서는 Discriminated Union을 적극적으로 활용하여 코드의 품질을 높이는 것이 좋습니다. 이를 통해 개발자는 더 명확하고 유지보수하기 쉬운 코드를 작성할 수 있으며, 타입 시스템의 이점을 최대한 활용할 수 있습니다.

728x90