본문으로 건너뛰기

교차 타입(Intersection)과 유니온 타입(Union)

질문​

TypeScript에서 교차 타입(intersection type)과 유니온 타입(union type)은 무엇이고 어떻게 다른가요?

답변 초안​

교차 타입은 &로 연결한 여러 타입을 모두 동시에 만족해야 하는 타입입니다.

즉 "이 값은 A이면서 동시에 B다"라는 AND 의미를 가지며, 객체 타입끼리 교차하면 양쪽의 프로퍼티를 모두 합친 타입이 만들어집니다.

유니온 타입은 |로 연결한 여러 타입 중 하나에 해당하면 되는 타입입니다.

즉 "이 값은 A이거나 B다"라는 OR 의미를 가지며, 값이 어느 쪽인지는 코드 흐름에 따라 달라집니다.

이 차이 때문에 멤버 접근 방식도 달라집니다. 교차는 모든 타입을 만족하므로 합쳐진 모든 프로퍼티에 접근할 수 있고, 유니온은 어느 쪽일지 확정되지 않아 모든 멤버가 공통으로 가진 프로퍼티만 바로 접근할 수 있습니다.

한 줄 비유 교차(&)는 "이것이면서 동시에 저것"입니다. 직원이면서 동시에 관리자인 사람처럼 양쪽 자격을 전부 가진 존재라, 양쪽 권한을 모두 쓸 수 있습니다. 유니온(|)은 "이거나 저거"입니다. 카드 결제 또는 현금 결제처럼 둘 중 무엇이 올지 모르니, 공통으로 보장되는 것만 믿고 쓸 수 있습니다.

교차 타입(Intersection Type)​

교차 타입은 &로 여러 타입을 결합하며, 그 값은 결합된 모든 타입을 동시에 만족해야 합니다.

객체 타입끼리 교차하면 양쪽의 프로퍼티를 모두 가진 하나의 타입이 됩니다.

type ProductItem = {
id: number;
name: string;
type: string;
price: number;
imageUrl: string;
quantity: number;
};

// ProductItem에 할인 정보(discountAmount)를 더한 타입
type ProductItemWithDiscount = ProductItem & { discountAmount: number };

const discounted: ProductItemWithDiscount = {
id: 1,
name: "후라이드 치킨",
type: "치킨",
price: 18_000,
imageUrl: "/fried-chicken.png",
quantity: 1,
discountAmount: 2_000, // 교차로 추가된 필드
};

// ProductItem의 모든 필드 + discountAmount에 모두 접근 가능
console.log(discounted.name, discounted.discountAmount);

교차 타입은 이렇게 여러 객체 타입을 합쳐 더 구체적인 타입을 만들 때 주로 사용합니다.

type WithTimestamp = { createdAt: Date; updatedAt: Date };
type Article = { title: string; body: string };

// 게시글 타입에 공통 타임스탬프 필드를 합친다
type StoredArticle = Article & WithTimestamp;

원시 타입을 교차하면 생기는 일​

교차는 "양쪽을 동시에 만족"을 의미하므로, 동시에 만족할 수 없는 원시 타입을 교차하면 never가 됩니다.

string이면서 동시에 number인 값은 존재할 수 없기 때문입니다.

type T = string & number; // 추론 결과: never

const value: T = "hello";
// Error: Type 'string' is not assignable to type 'never'.
const value2: T = 123;
// Error: Type 'number' is not assignable to type 'never'.

객체 타입에서도 같은 프로퍼티의 타입이 서로 충돌하면 그 프로퍼티가 never가 되어 사실상 값을 만들 수 없게 됩니다.

type A = { id: string };
type B = { id: number };

type C = A & B; // id: string & number → id: never

유니온 타입(Union Type)​

유니온 타입은 |로 여러 타입을 나열해 정의하며, 그 값은 나열된 타입 중 하나가 됩니다.

type Id = number | string;

function printId(id: Id) {
console.log(id); // number이든 string이든 출력은 가능
}

printId(101); // 가능
printId("202"); // 가능

유니온 값은 실행 시점에 어느 멤버일지 확정되지 않으므로, 모든 멤버가 공통으로 가진 프로퍼티/연산만 타입 좁히기(narrowing) 없이 바로 접근할 수 있습니다.

// 앞서 교차 타입 예시에서 정의한 ProductItem과, 카드 아이템 타입
type CardItem = {
id: number;
name: string;
type: string;
imageUrl: string;
};

// 상품(ProductItem)이거나 카드(CardItem)인 프로모션 아이템
type PromotionEventItem = ProductItem | CardItem;

const printPromotionItem = (item: PromotionEventItem) => {
console.log(item.name); // 가능: ProductItem과 CardItem 모두 name을 가짐

console.log(item.quantity);
// Error: Property 'quantity' does not exist on type 'PromotionEventItem'.
// (CardItem에는 quantity가 없어 바로 접근 불가)
};

타입 좁히기(Narrowing)​

유니온 멤버 고유의 프로퍼티를 쓰려면 먼저 어떤 타입인지 좁혀야 합니다.

원시 타입이 섞여 있으면 typeof로 구분합니다.

function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // 이 블록에서 id는 string
} else {
console.log(id.toFixed(2)); // 이 블록에서 id는 number
}
}

객체 타입이라면 특정 프로퍼티의 존재 여부를 in으로 확인할 수 있습니다. 앞서 item.quantity에서 났던 에러도 이렇게 좁히면 안전하게 접근할 수 있습니다.

// 앞서 정의한 PromotionEventItem(ProductItem | CardItem)을 사용
function printQuantity(item: PromotionEventItem) {
if ("quantity" in item) {
console.log(item.quantity); // 여기서 item은 ProductItem으로 좁혀짐
} else {
console.log(item.name); // 여기서 item은 CardItem
}
}

가장 권장되는 방식은 공통 리터럴 필드(discriminant)를 두고 그 값으로 분기하는 판별 유니온(discriminated union) 입니다.

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;

function area(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // shape는 Circle로 좁혀짐
case "square":
return shape.side ** 2; // shape는 Square로 좁혀짐
}
}

교차 vs 유니온 - 핵심 차이​

항목교차(&)유니온(|)
의미AND - 모든 타입을 동시에 만족OR - 멤버 중 하나
멤버 접근합쳐진 모든 프로퍼티에 접근 가능공통 프로퍼티만 바로 접근(이후 타입 좁히기 필요)
객체끼리 결합 결과양쪽 프로퍼티를 모두 합친 타입"둘 중 하나" 형태의 값을 표현
원시 타입 결합string & number는 never(불가능)string | number처럼 후보 집합
대표 용도객체 타입 확장·믹스인, 공통 필드 합치기여러 상태/케이스 표현, 판별 유니온

함께 쓰는 패턴​

교차로 객체 타입 확장하기​

교차 타입은 기존 객체 타입에 필드를 더해 확장하거나, 여러 타입을 믹스인처럼 합칠 때 유용합니다.

type Base = { id: string };
type Loggable = { log: () => void };

// Base에 로깅 능력을 더한 타입
type Entity = Base & Loggable;

interface의 extends와 목적은 비슷하지만, 교차 타입은 type 별칭이나 제네릭과 더 자유롭게 조합할 수 있다는 차이가 있습니다.

판별 유니온(discriminated union)​

공통 리터럴 필드로 분기하는 판별 유니온은 유니온 타입을 안전하게 다루는 대표 패턴입니다.

상태(state)나 응답 형태를 표현하는 실전 예시는 식별 가능한 유니온과 빠짐없이 분기 처리하기에 정리되어 있으니 그쪽을 참고하세요.

실무 주의점​

  • 원시 타입을 교차한 string & number는 never가 됩니다. 의도는 "둘 다 허용"인데 실수로 &를 쓰면 아무 값도 넣을 수 없는 타입이 만들어집니다(이때는 |가 맞습니다).
  • 교차 시 같은 이름의 프로퍼티 타입이 충돌하면 해당 프로퍼티가 never가 되어 객체 자체를 만들 수 없게 됩니다. 합치려는 타입들의 프로퍼티 이름이 겹치지 않는지 확인하세요.
  • interface extends와 교차 타입은 비슷해 보이지만, extends는 충돌 시 컴파일 에러로 즉시 알려주는 반면 교차 타입은 조용히 never로 만들어 버려 원인 파악이 늦어질 수 있습니다.
  • 유니온은 공통 프로퍼티만 바로 접근할 수 있으므로, 멤버 고유 프로퍼티를 쓰려면 typeof·in·판별 필드 등으로 반드시 타입 좁히기(narrowing)해야 합니다.
  • 유니온은 조건부 타입·매핑 타입에서 분배(distribution) 동작을 합니다. 예를 들어 T extends U ? X : Y에서 T가 유니온이면 각 멤버에 대해 따로 평가된 뒤 합쳐지므로, 의도와 다른 결과가 나올 수 있습니다.

면접 답변​

"교차 타입(&)은 여러 타입을 동시에 만족해야 하는 AND 타입이고, 유니온 타입(|)은 여러 타입 중 하나에 해당하는 OR 타입입니다. 그래서 교차는 합쳐진 모든 프로퍼티에 접근할 수 있고, 유니온은 모든 멤버가 공통으로 가진 프로퍼티만 바로 접근할 수 있어 나머지는 typeof·in·판별 필드로 타입 좁히기(narrowing)해야 합니다. 교차는 객체 타입을 확장하거나 합칠 때, 유니온은 상태나 케이스를 표현하는 판별 유니온에 주로 씁니다. 주의할 점으로 string & number처럼 동시에 만족할 수 없는 원시 타입을 교차하면 never가 된다는 점이 있습니다."