Web

Typescript: Union 형식, Intersection 형식, 상속

시유후 2025. 1. 24. 20:39

이 글에선 Typescript에서 사용되는 형식인 Union 형식, Intersection 형식에 대해서 알아보고, 
필자가 가졌던 의문인 Union형식을 사용할거면, 상속을 사용하는 것이 낫지 않은가? 에 대해 설명을 하고,
Union 형식이 사용되는 간단한 예시를 소개할 예정이다. 
 

Union형식과 Intersection형식

Union 형식

Union 형식은 여러 타입 중 하나의 타입을 가질 수 있도록 정의한다. 이는 상속의 개념에서 부모 클래스와 비슷하게 동작한다.  연산자를 사용해 여러 타입을 조합할 수 있다.
 
예를 들어, Circle과 Rectangle을 대표하는 Shape 타입을 Union 형식으로 정의할 수 있다.

type Circle = {
    radius: number;
};

type Rectangle = {
    x: number;
    y: number;
};

type Shape = Circle | Rectangle;

function area(shape: Shape) {
    // 계산..
}

 
위와 같은 상황 뿐만 아니라 아래처럼 기본형으로도 유니온형식을 만들 수 있다.

function foo(bar: number | string){
 //
}

 

kind 속성

위처럼 구현하게 되면, 좋다 부모클래스를 만든것 까지는 좋다 하지만, area 내부에서 대체 이들을 어떻게 구별할 것인가? 위의 예제에서는 Circle과 Rectangle 구분하기 쉽기 때문에 아래와 같이 구분할 수 있다. 

function area(shape: Shape){
  if("radius" in shape) { // Circle의 경우
    return Math.PI*shape.radius*shape.radius;
  } else { // 나머지 즉 Rectangle의 경우
    return shape.x*shape.y;
  }
}

 
하지만 유니온 형식에 포함되는 타입이 많아져서 프로퍼티가 구별하기 힘들정도라면? 어떻게 구별할 것인가. 그래서, kind 속성이 등장했다. 유니온형식의 요소를 구별하기 위함이다.

type Circle = {
    radius: number;
    kind: "circle";
};

type Rectangle = {
    x: number;
    y: number;
    kind: "rectangle"
};

type Shape = Circle | Rectangle;


위 방식을 통해 유니온형식의 구성요소 하나하나를 구별할 수 있고, switch문을 사용해 아래와 같이 함수를 작성할 수 있다.

function area(shape: Shape){
  switch (shape.kind) { // kind를 통해 구성요소를 구별
    case "circle": 
      return Math.PI*shape.radius*shape.radius;
    case "rectangle"
      return shape.x*shape.y;
    default:
      throw Error("불가능한 방식입니다.");
  }
}

Intersection 형식

Intersection형식은 자식클래스를 만드는것에 비유할 수 있다.   &  키워드를 통해 지정한다. 또한 예시를 통해 살펴보자

type Toy = {
  price: number;
  quantity: number;
};

type BoardGame = Toy & {
  players: number;
};

type Puzzle = Toy & {
  pieces: number;
};

 
 

vs 상속

TypeScript를 공부하며, "Union 형식을 사용할 바에 차라리 상속을 사용하는 것이 더 나은 선택이 아닐까?"라는 의문이 들었다. 하지만 Union 형식과 상속은 서로 다른 상황에서 더 적합한 방법으로 사용될 수 있다. 이를 가독성과 확장성 측면에서 비교해 보았다.
 

가독성

Union 형식은 코드가 간결하고 직관적이다. 특히 타입이 간단한 경우, 클래스 문법을 사용하는 것보다 가독성이 좋다.
 
아래 두 코드를 비교했을때 Union 형식을 사용하는 쪽이 어떤 type이 있는지, 어떤 동작을하는지 한눈에 알아보기가 쉽다. 상속을 사용하는 경우 코드의 규모가 커질수록 복잡하고 알아보기 힘들어진다.

// Union 형식

type Shape = 
  {kind: "circle"; radius: number} |
  {kind: "rectangle"; x: number; y: number} |
  {kind: "triangle"; base: number; height: number};
  
function area(shape: Shape){
  switch (shape.kind) { // kind를 통해 구성요소를 구별
    case "circle": 
      return Math.PI*shape.radius*shape.radius;
    case "rectangle":
      return shape.x*shape.y;
    case "triangle":
      return shape.base*shape.height/2;
    default:
      throw Error("불가능한 방식입니다.");
  }
}
// 상속

interface Shape {
  area(): number;
}

class Circle implements Shape{
  constructor(public radius: number){}
  area(): number {
    return Math.PI*this.radius*this.radius;
  }
}

class Rectangle implements Shape{
  constructor(public x: number; public y: number;){}
  area(): number {
    return this.x*this.y;
  }
}

class Triangle implements Shape{
  constructor(public base: number; public height: number){}
  area(): number {
    return this.base*this.height/2;
  }
}

 

확장성

확장성의 면에서도 이점이 있다. type과 Union 형식은 OOP의 철학을 따르진 않지만, OCP(개방폐쇄의 원칙)을 자연스럽게 만족한다.  새로운 타입을 추가할 때 기존 코드를 수정할 필요 없이 새로운 타입만 정의하면 된다.

type Shape = 
  {kind: "circle"; radius: number} |
  {kind: "rectangle"; x: number; y: number} |
  {kind: "triangle"; base: number; height: number} |
  {kind: "square"; x: number}; // 새로운 타입을 추가
  
  function area(shape: Shape){
  switch (shape.kind) { // kind를 통해 구성요소를 구별
    case "circle": 
      return Math.PI*shape.radius*shape.radius;
    case "rectangle":
      return shape.x*shape.y;
    case "triangle":
      return shape.base*shape.height/2;
    case "square": // 새로 추가된 타입에 대한 처리
      return shape.x*shape.x;
    default:
      throw Error("불가능한 방식입니다.");
  }
}

 
 

결론

Union형식은 때에 따라선 OOP를 따르지 않고 사용할 수 있는 하나의 방법이다.

  • 데이터 중심적 모델, 메서드(즉, 행동)이 중점이 아닌 상황
  • 그 구조가 매우 간단하여 class문법을 쓰기엔 오버헤드가 큰 상황

결론적으로, Union 형식은 OOP를 따르지 않아도 효율적이고 간단한 해결책을 제공할 수 있는 강력한 도구이다. 상황에 따라 Union 형식을 선택하면 코드의 간결성과 확장성을 모두 확보할 수 있다.
 
위 내용을 공부하면서 내가 너무 OOP에 갇혀있다는 생각을 했다. type과 Union을 보면서 이는 OOP의 원칙을 만족하는가 (애초에 Union은 OOP가 말하는 객체조차도 아니었지만) 와 같은 생각을 하며 계속 Union에서 OOP의 편린을 찾으려고 애를 썼다. 처음에는 심지어 Union 형식을 보며 화도 났다. 어떻게 이렇게 코드를 짤 수 있냐며. 위 내용을 공부하면서 내 갇혀있던 생각도 좀 더 넓게 생각하게 될 수 있어진 것 같다.