iBetter Books
수정

이것 그리고 저것 — 인터섹션 타입

로봇 만화에는 합체하는 로봇이 자주 등장합니다. 비행 로봇과 전투 로봇이 합체하면, 날 수도 있고 싸울 수도 있는 새로운 로봇이 탄생합니다. 어느 하나의 능력을 포기하는 게 아니라, 둘 다 가집니다.

TypeScript의 인터섹션 타입이 바로 그 합체입니다. A & B라고 쓰면 A의 모든 속성과 B의 모든 속성을 동시에 가져야 하는 새 타입이 만들어집니다. 유니온이 "둘 중 하나"라면, 인터섹션은 "둘 다 모두"입니다.

& 연산자로 인터섹션 타입 만들기

& 기호로 두 타입을 연결합니다.

interface Flyable {  fly(): void;  altitude: number;}interface Swimmable {  swim(): void;  depth: number;}type AquaticFlyer = Flyable & Swimmable;const duck: AquaticFlyer = {  altitude: 100,  fly() {    console.log("날개를 펄럭입니다.");  },  depth: 2,  swim() {    console.log("물갈퀴로 헤엄칩니다.");  }};

AquaticFlyerFlyableflyaltitude, 그리고 Swimmableswimdepth를 모두 가져야 합니다. 하나라도 빠지면 TypeScript가 오류를 냅니다.

실용 예제 — 기본 정보와 직원 정보 합치기

인터섹션 타입이 실무에서 가장 자주 쓰이는 상황을 봅시다. 시스템에 사람에 대한 기본 정보 타입이 있고, 직원에게만 필요한 정보 타입이 따로 있을 때, 이 둘을 합쳐 직원 타입을 만들 수 있습니다.

interface PersonInfo {  name: string;  age: number;  email: string;}interface EmployeeInfo {  employeeId: string;  department: string;  startDate: Date;}type Employee = PersonInfo & EmployeeInfo;const employee: Employee = {  name: "김도현",  age: 30,  email: "[email protected]",  employeeId: "EMP-2024-001",  department: "개발팀",  startDate: new Date("2024-03-01")};

PersonInfoEmployeeInfo는 각각 독립적으로 존재하면서, 필요할 때 &로 합쳐 Employee 타입을 만듭니다. 만약 PersonInfo가 고객 관리 시스템에서도 쓰인다면, 같은 인터페이스를 재사용해 Customer = PersonInfo & CustomerInfo를 만들 수 있습니다.

인터섹션 타입에 값을 추가하기

기존 타입에 속성을 몇 개만 추가하고 싶을 때도 인터섹션 타입이 유용합니다.

interface Todo {  id: number;  title: string;  completed: boolean;}type TodoWithTimestamp = Todo & {  createdAt: Date;  updatedAt: Date;};const todo: TodoWithTimestamp = {  id: 1,  title: "TypeScript 공부하기",  completed: false,  createdAt: new Date(),  updatedAt: new Date()};

별도의 이름 있는 인터페이스를 만들지 않고, 인라인으로 추가 속성을 정의해서 합쳤습니다. 한 번만 쓰이는 경우라면 이렇게 간결하게 처리할 수 있습니다.

extends와의 비교

앞 챕터에서 배운 인터페이스 extends와 인터섹션 타입은 비슷해 보이지만 미묘한 차이가 있습니다.

// extends 방식interface AdminExtends extends User {  role: string;}// 인터섹션 방식type AdminIntersection = User & {  role: string;};

결과적으로 둘 다 User의 모든 속성에 role이 추가된 타입이 됩니다. 그러나 속성 타입이 충돌하는 경우에 차이가 생깁니다.

interface A {  value: string;}interface B {  value: number;}// extends 사용 시 — 오류 발생// interface C extends A, B {}  // 오류: 'value' 속성 타입이 호환되지 않습니다// 인터섹션 사용 시 — 오류 없이 타입 생성, 하지만 결과가 nevertype C = A & B;// C.value의 타입은 string & number = never (이 타입을 만족하는 값은 없습니다)

string & number는 문자열이면서 동시에 숫자인 값인데, 그런 값은 존재하지 않으므로 never 타입이 됩니다. extends는 충돌 시 컴파일 오류를 내지만, 인터섹션은 오류 없이 never가 되어 실수를 놓칠 수 있습니다. 이 점을 주의해야 합니다.

일반적인 권장 사항은 다음과 같습니다.

  • 객체 지향적으로 구조를 설계하고 확장할 때는 extends
  • 두 타입을 유연하게 합치거나 일회성으로 속성을 추가할 때는 &

유니온 vs 인터섹션 — 한눈에 비교

두 연산자의 차이를 집합 개념으로 이해하면 더 명확합니다.

집합 A = { name: string; age: number } 집합 B = { email: string; role: string }

A | B(유니온) — A이거나 B이면 됩니다. A만 만족해도, B만 만족해도 됩니다. 따라서 두 타입에 공통으로 있는 속성만 안전하게 사용할 수 있습니다.

A & B(인터섹션) — A이면서 동시에 B여야 합니다. 두 타입의 모든 속성을 가져야 합니다.

type PersonInfo = {  name: string;  age: number;};type ContactInfo = {  email: string;  phone: string;};type PersonOrContact = PersonInfo | ContactInfo;type PersonAndContact = PersonInfo & ContactInfo;// PersonOrContact: name과 age만 가져도 되고, email과 phone만 가져도 됩니다// PersonAndContact: name, age, email, phone 모두 있어야 합니다const onlyPerson: PersonOrContact = {  name: "이민준",  age: 22};const full: PersonAndContact = {  name: "이민준",  age: 22,  email: "[email protected]",  phone: "010-1234-5678"};

유니온과 인터섹션으로 타입을 조합하는 법을 배웠습니다. 그런데 처음 배울 때 한 가지 의문이 생깁니다. TypeScript에는 타입을 정의하는 방법이 두 가지 있습니다. interfacetype alias 입니다. 언제 어느 것을 써야 할까요? 다음 챕터에서 두 방식의 차이를 정리하고 실무에서의 선택 기준을 알아봅니다.