어제 오늘 내일

[TypeScript] 6. 유니온과 인터섹션, 그리고 리터럴 타입 본문

IT/TypeScript

[TypeScript] 6. 유니온과 인터섹션, 그리고 리터럴 타입

hi.anna 2026. 8. 10. 18:18

이 글의 목표: TypeScript 타입 시스템의 뼈대인 "합집합(union)"과 "교집합(intersection)"을 이해하고, 실전 패턴을 익힙니다.

유니온(|): "이것 또는 저것"

여러 타입 중 하나일 수 있음을 나타냅니다.

// id는 문자열이거나 숫자일 수 있다
type ID = string | number;

let userId: ID = 100;
userId = "abc-123"; // 둘 다 허용

유니온의 값을 쓸 때는, 모든 타입에 공통으로 존재하는 것만 바로 접근할 수 있습니다.

function printId(id: string | number) {
  // id.toUpperCase(); // ❌ number에는 toUpperCase가 없다
  console.log(id.toString()); // ✅ string, number 모두 toString이 있다

  // 각각 다르게 처리하려면 타입을 좁혀야 한다 (7편에서 자세히)
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // 여기선 string 확정
  } else {
    console.log(id.toFixed(0));    // 여기선 number 확정
  }
}

리터럴 유니온: 가장 실용적인 패턴

특정 값들만 허용하는 유니온은 실무에서 정말 자주 씁니다. enum 대신 즐겨 쓰이죠.

// 버튼의 크기를 세 가지로 제한
type ButtonSize = "small" | "medium" | "large";

function Button(size: ButtonSize) { /* ... */ }

Button("medium");  // ✅
// Button("huge");  // ❌ 허용되지 않은 값 → 오타·잘못된 값 방지

숫자 리터럴도 됩니다.

type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 4;
// roll = 7; // ❌

인터섹션(&): "이것 그리고 저것"

여러 타입을 모두 만족해야 함을 나타냅니다. 즉, 속성들을 합칩니다.

type HasName = { name: string };
type HasAge = { age: number };

// 두 타입을 모두 만족 → name과 age를 둘 다 가져야 한다
type Person = HasName & HasAge;

const p: Person = { name: "철수", age: 30 }; // 둘 다 있어야 함
// const p2: Person = { name: "영희" };      // ❌ age 누락

인터섹션은 여러 작은 타입을 조합해 큰 타입을 만들 때 유용합니다.

type Timestamps = { createdAt: Date; updatedAt: Date };
type Identifiable = { id: string };

// 공통 속성을 재사용해 여러 엔티티를 조립한다
type Article = Identifiable & Timestamps & {
  title: string;
  body: string;
};

판별 유니온(Discriminated Union): 실전의 꽃

서로 다른 형태의 객체들을 하나의 유니온으로 묶되, 공통의 "태그" 속성으로 구분하는 패턴입니다. 상태 관리, API 응답 처리에 강력합니다.

// 각 타입은 kind라는 공통 태그를 가진다 (값은 서로 다름)
type Circle = { kind: "circle"; radius: number };
type Rectangle = { kind: "rectangle"; width: number; height: number };
type Triangle = { kind: "triangle"; base: number; height: number };

type Shape = Circle | Rectangle | Triangle;

function getArea(shape: Shape): number {
  // kind를 확인하면 TypeScript가 나머지 속성을 자동으로 좁혀준다
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // 여기선 radius 접근 가능
    case "rectangle":
      return shape.width * shape.height;  // 여기선 width, height 접근 가능
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

kind 값을 확인하는 것만으로 TypeScript가 해당 분기 안에서 정확한 타입을 알려줍니다. 잘못된 속성에 접근하면 즉시 에러가 납니다.

실전 예제: API 응답 상태

로딩·성공·실패 상태를 판별 유니온으로 표현하면 아주 깔끔합니다.

type ApiState<T> =
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

function render(state: ApiState<string[]>) {
  switch (state.status) {
    case "loading":
      return "불러오는 중...";
    case "success":
      // 여기서만 data에 접근 가능 → data 없이 접근하는 실수를 막는다
      return `${state.data.length}개 로드됨`;
    case "error":
      // 여기서만 message에 접근 가능
      return `에러: ${state.message}`;
  }
}

loading 상태에서 실수로 state.data에 접근하려 하면 컴파일러가 막아줍니다. 이것이 판별 유니온이 안전한 이유입니다.

정리

  • 유니온(|) 은 "여러 타입 중 하나". 리터럴 유니온으로 값을 제한하는 패턴이 특히 유용하다.
  • 인터섹션(&) 은 "여러 타입을 모두 만족". 작은 타입을 조합해 큰 타입을 만든다.
  • 판별 유니온은 공통 태그로 구분되는 유니온으로, 상태·응답 처리에서 안전하고 강력하다.

다음 글에서는 유니온을 실제로 다루는 핵심 기술, "타입 좁히기"를 집중적으로 파고듭니다.

반응형
Comments