안동민 개발노트

본문 시작

유니온과 인터섹션 타입

여러 후보 중 하나를 허용하는 유니온과 모든 구조를 결합하는 인터섹션을 구분해 API 응답과 기능 조합을 모델링합니다.

프로그래밍에서는 하나의 변수가 여러 타입 중 어떤 하나가 될 수도 있고, 여러 타입의 특징을 모두가져야 할 때도 있습니다.

타입스크립트는 이러한 복잡한 시나리오를 효과적으로 다룰 수 있도록 유니온(Union) 타입과 인터섹션(Intersection) 타입이라는 강력한 도구를 제공합니다.

이 절에서는 이 두 가지 특수 타입이 무엇이며, 어떻게 활용되는지 자세히 살펴보겠습니다.

유니온과 인터섹션의 이중성

VALUE SET · USABLE MEMBERS

유니온과 인터섹션의 이중성

값 후보와 즉시 사용할 수 있는 멤버는 반대 방향으로 움직인다. 인터섹션은 런타임 객체 병합이 아니라 정적 제약이다.

유니온과 인터섹션의 이중성 union과 intersection을 가능한 값 범위, 즉시 사용 가능한 멤버, 값 생성 요구, 충돌 결과의 네 축으로 대칭 비교하고 정적 제약과 런타임 병합을 구분한다. A | B값 후보가 넓어짐A 또는 B 값A & B값 후보가 좁아짐A와 B를 동시에 만족CONSUME공통 멤버만 사용먼저 좁히기CONSTRUCT모든 요구를 충족각 계약의 필수 멤버CONFLICT분기 후 안전typeof · in · discriminantCONFLICTnever 가능판별 충돌은 전체 never
  1. 값 후보가 넓어짐

    A | B — A 또는 B 값

  2. 값 후보가 좁아짐

    A & B — A와 B를 동시에 만족

  3. 공통 멤버만 사용

    CONSUME — 먼저 좁히기

  4. 모든 요구를 충족

    CONSTRUCT — 각 계약의 필수 멤버

  5. 분기 후 안전

    CONFLICT — typeof · in · discriminant

  6. never 가능

    CONFLICT — 판별 충돌은 전체 never

OR/AND 비유만 외우지 말고 값 집합, 멤버 집합, 값 생성 요구를 나누어 판단한다.


유니온 타입 (Union Types)

유니온 타입은 변수나 매개변수가 여러 타입 중 하나를 가질 수 있도록 정의할 때 사용합니다.

파이프(|) 기호를 사용하여 여러 타입을 연결하며, "A 이거나 B 이다"라는 의미를 가집니다.

이는 특정 값이 다양한 형태를 가질 수 있는 유연성을 제공하면서도, 여전히 타입 안정성을 유지할 수 있도록 돕습니다.

예를 들어 웹사이트에서 사용자 ID가 숫자일 수도 있고, 문자열일 수도 있다고 가정해봅시다.

// string 또는 number 타입이 될 수 있는 UserId 타입을 정의합니다.
type UserId = string | number;

let user1Id: UserId = "abc123";  // 문자열 할당 가능
let user2Id: UserId = 456789;    // 숫자 할당 가능

// 오류: UserId 타입은 boolean을 허용하지 않습니다.
// let user3Id: UserId = true; // Error: Type 'boolean' is not assignable to type 'UserId'.

function printId(id: UserId) {
  console.log(`The ID is: ${id}`);

  // 유니온 타입의 값에 접근할 때는, 해당 타입들의 공통 속성만 직접 접근 가능합니다.
  // console.log(id.toUpperCase()); // Error: 'id'는 'string | number' 타입이므로 'toUpperCase' 속성이 없습니다.
                                 // number 타입에는 toUpperCase()가 없기 때문입니다.

  // 타입을 좁히는(Narrowing) 과정을 통해 특정 타입의 속성에 접근할 수 있습니다.
  if (typeof id === "string") {
    // 이 블록 안에서 id는 string 타입으로 추론됩니다.
    console.log(`ID in uppercase: ${id.toUpperCase()}`);
  } else {
    // 이 블록 안에서 id는 number 타입으로 추론됩니다.
    console.log(`ID fixed to 2 decimal places: ${id.toFixed(2)}`);
  }
}

printId("uniqueUser123"); // The ID is: uniqueUser123, ID in uppercase: UNIQUEUSER123
printId(789.123);         // The ID is: 789.123, ID fixed to 2 decimal places: 789.12
유니온 타입의 핵심

유연성: 하나의 변수가 여러 가지 가능한 타입을 가질 수 있습니다.

안전성: 유니온 타입의 변수에 접근할 때는 모든 유니온 멤버 타입에 공통적으로 존재하는 속성/메서드만 직접 접근할 수 있습니다.

예를 들어 string | number 타입 변수에는 length 속성이나 toFixed() 메서드에 직접 접근할 수 없습니다.

타입 좁히기(Narrowing): typeof, instanceof, in 연산자 또는 사용자 정의 타입 가드(Type Guards)와 같은 방법을 사용하여 코드 블록 내에서 변수의 타입을 특정 타입으로 좁힐(Narrow) 수 있습니다.

이렇게 타입을 좁히고 나면 해당 타입에 특화된 속성이나 메서드를 안전하게 사용할 수 있습니다.


인터섹션 타입 (Intersection Types)

인터섹션 타입은 여러 타입의 모든 속성을 결합하여 새로운 타입을 만들 때 사용합니다.

앰퍼샌드(&) 기호를 사용하여 여러 타입을 연결하며, "A 이고 B 이다" 또는 "A와 B의 모든 특징을 가진다"라는 의미를 가집니다.

이는 여러 인터페이스나 객체 타입의 속성들을 하나로 합쳐서 더 풍부한 단일 타입을 정의할 때 매우 유용합니다.

// Employee 인터페이스 정의
interface Employee {
  employeeId: string;
  department: string;
}

// Person 인터페이스 정의 (2장 3절에서 사용했던 것과 유사)
interface Person {
  name: string;
  age: number;
}

// Employee와 Person 인터페이스의 모든 속성을 가진 Manager 타입을 정의합니다.
type Manager = Employee & Person;

let manager1: Manager = {
  employeeId: "MGR001",
  department: "Sales",
  name: "이지혜",
  age: 45,
};

console.log(manager1.name);         // 이지혜
console.log(manager1.employeeId);   // MGR001

// 오류: Manager 타입이 요구하는 모든 속성을 포함해야 합니다.
// let incompleteManager: Manager = {
//   name: "김철수",
//   age: 30
// }; // Error: Property 'employeeId' is missing in type '{ name: string; age: number; }' but required in type 'Manager'.
인터섹션 타입의 핵심

결합: 여러 타입이 가진 모든 멤버(속성, 메서드)를 새로운 하나의 타입으로 합칩니다.

강화: 기존 타입에 새로운 속성을 추가하는 것처럼 활용할 수 있습니다.

예를 들어, BaseType & { newProp: string } 와 같이 기존 타입에 newProp 속성을 추가한 새로운 타입을 만들 수 있습니다.

충돌 처리: 같은 속성은 양쪽 타입을 동시에 만족해야 합니다. 공통 값이 없는 string & number는 never가 되지만 string & "ready"는 "ready"가 됩니다. 판별 속성의 충돌은 객체 전체가 never로 축소될 수도 있습니다.


유니온과 인터섹션 타입의 활용 예시

두 타입은 실제 애플리케이션 개발에서 매우 자주 사용됩니다.

예시 1: API 응답 처리 (유니온)

서버로부터 받아오는 데이터가 성공 응답일 수도 있고, 오류 응답일 수도 있다고 가정해봅시다.

interface SuccessResponse {
  status: "success";
  data: any; // 실제 데이터 타입은 더 구체적으로 정의할 수 있습니다.
}

interface ErrorResponse {
  status: "error";
  message: string;
  code: number;
}

// API 응답은 성공 응답이거나 오류 응답 중 하나입니다.
type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === "success") {
    // 여기서는 response가 SuccessResponse 타입으로 좁혀집니다.
    console.log("데이터를 성공적으로 받았습니다.", response.data);
  } else {
    // 여기서는 response가 ErrorResponse 타입으로 좁혀집니다.
    console.log(`오류 발생 (${response.code}): ${response.message}`);
  }
}

// 함수 호출 예시
handleResponse({ status: "success", data: { user: "Alice" } });
handleResponse({ status: "error", message: "인증 실패", code: 401 });

이처럼 status와 같은 식별 가능한 속성(Discriminant Property)으로 유니온을 좁힐 수 있습니다. 위 코드는 success·error 두 상태를 처리하며 입력 검증과 never 검사는 구현하지 않았습니다.

다음 그림은 여기에 로딩 상태를 더해 loading·success·failure로 나누고, 외부 입력 검증과 누락 검사를 추가한 확장 설계입니다.

검증된 결과와 완전한 상태 처리

UNKNOWN · VALIDATE · EXHAUSTIVE

검증된 결과와 완전한 상태 처리

외부 unknown을 검증해 판별 유니온으로 만든 뒤 status switch가 상태별 필드와 never 완전성 검사로 이어진다.

검증된 결과와 완전한 상태 처리 unknown API payload를 런타임 검증해 Loading/Success/Failure discriminated union으로 만들고 status switch가 각 전용 필드와 never exhaustiveness로 이어지는 처리 계약을 보여 준다. INPUTunknown payload타입 선언만 믿지 않음VALIDATE런타임 검증형태·필드·값 확인LOADINGstatus: loading진행 정보만 사용SUCCESSstatus: successdata 전용 필드FAILUREstatus: failuremessage 전용 필드NEVERexhaustive check새 상태 누락을 진단
  1. unknown payload

    INPUT — 타입 선언만 믿지 않음

  2. 런타임 검증

    VALIDATE — 형태·필드·값 확인

  3. status: loading

    LOADING — 진행 정보만 사용

  4. status: success

    SUCCESS — data 전용 필드

  5. status: failure

    FAILURE — message 전용 필드

  6. exhaustive check

    NEVER — 새 상태 누락을 진단

타입은 검증 뒤의 내부 계약을 강화한다. 외부 응답을 any로 받거나 단언하면 판별 유니온의 안전성도 함께 사라진다.

예시 2: 기능 결합 (인터섹션)

특정 사용자가 일반 사용자 기능과 관리자 기능을 모두 가지고 있다고 정의하고 싶을 때.

interface User {
  id: string;
  email: string;
}

interface AdminPrivileges {
  manageUsers: () => void;
  deleteContent: () => void;
}

// User의 속성과 AdminPrivileges의 속성을 모두 가진 슈퍼 유저
type SuperUser = User & AdminPrivileges;

let superAdmin: SuperUser = {
  id: "super_admin_001",
  email: "[email protected]",
  manageUsers: () => console.log("사용자를 관리합니다."),
  deleteContent: () => console.log("콘텐츠를 삭제합니다."),
};

console.log(superAdmin.email);
superAdmin.manageUsers();

SuperUser 타입은 User 타입의 id, email 속성과 AdminPrivileges 타입의 manageUsers, deleteContent 메서드를 모두 포함하게 됩니다.


유니온과 인터섹션은 둘 다 타입을 조합하지만, 의미는 정반대입니다.


유니온 타입과 인터섹션 타입은 타입스크립트의 유연성과 표현력을 크게 향상시키는 중요한 개념입니다.

유니온은 이것 또는 저것의 관계를, 인터섹션은 이것과 저것 모두의 관계를 정의할 때 사용된다는 점을 명확히 이해하는 것이 중요합니다.

이 두 가지를 적절히 활용하면 더욱 강력하고 안정적인 타입을 설계할 수 있습니다.