icon
2장 : 타입스크립트 기본 타입

유니온과 인터섹션 타입


프로그래밍에서는 하나의 변수가 여러 타입 중 어떤 하나가 될 수도 있고, 여러 타입의 특징을 모두 가져야 할 때도 있습니다. 타입스크립트는 이러한 복잡한 시나리오를 효과적으로 다룰 수 있도록 유니온(Union) 타입인터섹션(Intersection) 타입이라는 강력한 도구를 제공합니다.

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


유니온 타입 (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

유니온 타입의 핵심

  1. 유연성: 하나의 변수가 여러 가지 가능한 타입을 가질 수 있습니다.
  2. 안전성: 유니온 타입의 변수에 접근할 때는, 모든 유니온 멤버 타입에 공통적으로 존재하는 속성/메서드만 직접 접근할 수 있습니다. 예를 들어 string | number 타입 변수에는 length 속성이나 toFixed() 메서드에 직접 접근할 수 없습니다.
  3. 타입 좁히기(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'.

인터섹션 타입의 핵심

  1. 결합: 여러 타입이 가진 모든 멤버(속성, 메서드)를 새로운 하나의 타입으로 합칩니다.
  2. 강화: 기존 타입에 새로운 속성을 추가하는 것처럼 활용할 수 있습니다. 예를 들어, BaseType & { newProp: string } 와 같이 기존 타입에 newProp 속성을 추가한 새로운 타입을 만들 수 있습니다.
  3. 충돌 처리: 만약 인터섹션하는 두 타입이 동일한 이름의 속성을 가지지만 타입이 서로 다르면, 그 속성의 타입은 never가 됩니다. (예: { a: string } & { a: number }a 속성은 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) 을 활용하여 유니온 타입의 실제 타입을 안전하게 좁히는 패턴은 매우 강력합니다.

예시 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: "admin@example.com",
  manageUsers: () => console.log("사용자를 관리합니다."),
  deleteContent: () => console.log("콘텐츠를 삭제합니다."),
};

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

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


유니온 타입과 인터섹션 타입은 타입스크립트의 유연성과 표현력을 크게 향상시키는 중요한 개념입니다. 유니온은 '이것 또는 저것'의 관계를, 인터섹션은 '이것과 저것 모두'의 관계를 정의할 때 사용된다는 점을 명확히 이해하는 것이 중요합니다. 이 두 가지를 적절히 활용하면 더욱 강력하고 안정적인 타입을 설계할 수 있습니다.