이것 그리고 저것 — 인터섹션 타입
로봇 만화에는 합체하는 로봇이 자주 등장합니다. 비행 로봇과 전투 로봇이 합체하면, 날 수도 있고 싸울 수도 있는 새로운 로봇이 탄생합니다. 어느 하나의 능력을 포기하는 게 아니라, 둘 다 가집니다.
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("물갈퀴로 헤엄칩니다."); }};
AquaticFlyer는 Flyable의 fly와 altitude, 그리고 Swimmable의 swim과 depth를 모두 가져야 합니다. 하나라도 빠지면 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")};
PersonInfo와 EmployeeInfo는 각각 독립적으로 존재하면서, 필요할 때 &로 합쳐 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에는 타입을 정의하는 방법이 두 가지 있습니다. interface와 type alias 입니다. 언제 어느 것을 써야 할까요? 다음 챕터에서 두 방식의 차이를 정리하고 실무에서의 선택 기준을 알아봅니다.