My Boundary As Much As I Experienced

가짜정보가 판을 치길래 직접 써보고 정리한 인터섹션 타입(Intersection Type) 총정리 본문

FrontEnd/TypeScript

가짜정보가 판을 치길래 직접 써보고 정리한 인터섹션 타입(Intersection Type) 총정리

Bumang 2024. 5. 4. 02:27

타입스크립트에서 & 연산자를 쓰는 경우는 두 가지 있다.

그런데 수많은 블로그나 포스트에서 인터섹션 타입이나 인터페이스 결합에 대해 잘못 설명하고 있는 경우가 많다.

타입스크립트에서 관련 로직이 한 번 바뀌기라도 했나 의심될 정도다..

하여튼 chatGPT도 이를 참조해 틀린 얘기를 많이 한다.

계속 불분명한 지식이 돌아다니면서 나를 헷갈리게 해서 이참에 정리해봤다.

 

1. 인터섹션 타입으로 '객체형이 아닌' 타입들을 조합할 때 (교집합)

여타 블로그에서 인터섹션 타입이 합집합이라고만 해서 '그냥 언제나 합집합이구나.' 하면 안 된다.

chatGPT도 그걸 참조하는지 합집합이 아닌 경우에도 합집합이라 하는 경우가 있더라..

엄밀히 말하자면 intersection이라는 단어 자체가 '교집합'을 의미한다.

 

예를 들어 아래 예시의 유니온 타입 type1유니온 타입 type2

intersection type(교집합)으로 묶은 type3는 "b"밖에 될 수 없다.

type type1 = "a" | "b";
type type2 = "b" | "c";

type type3 = type1 & type2;

const typed3: type3 = "b";
console.log(typed3); // b

 

2. 인터섹션 타입으로 '객체형이 아니고 공통분모도 없는' 타입들을 조합할 때(never)

아예 교집합이 없는 타입들을 &으로 묶어버리면 어떻게 될까?

아래처럼 아예 never가 나온다. never인 경우는 값 할당 자체가 안 된다.

교집합 없는 것들끼리 합쳐보겠다고 인터섹션 타입으로 만들어버리면 never만 나온다는걸 명심해라...

type type1 = "a" | "b";
type type2 = "c" | "d";

type type3 = type1 & type2; // never

// never여서 아예 할당이 안 된다.
let typed3: type3; // 에러

// 할당이 안 되어 console에 찍을 수도 없다.
// console.log(typed3);

 

그러나 1번 예시처럼 공통분모가 있다면, 교집합이 추려진다.

type typeA = "a";
type typeB = "a" | "b";

type typeC = typeA & typeB; // a

const typed3: type3 = "a"

 

3. 인터섹션 타입으로 객체형 타입들을 조합할 때 (이 때만 합집합! 비스무리)

이때야말로 합집합이 나온다. 겹치지 않는 속성들을 모두 취해서 종합적으로 가져간다.

그러나 합집합 '비스무리'라고 한 이유가 있으니, 그건 바로 두 타입에서 겹치는 '키값'이 존재한다면, 해당 속성의 교집합만 취해진다.

// 객체형 타입끼리 &로 합치면 공통되는 속성이 없어도 모두 합쳐진다.
type AgentX = { x: number };
type AgentY = { y: number };
type AgentXY = PositionX & PositionY;

const PositionXY: PositionXY = { x: 1, y: 1 }; // 가능


// 객체형 타입끼리 &로 합칠 때 중복되는 속성이 있어도 괜찮다.
type PositionX = { x: number; z: number };
type PositionY = { y: number; z: number };
type PositionXY = PositionX & PositionY;

const PositionXY: PositionXY = { x: 1, y: 1, z: 1 }; // 가능

 

아래 예시를 보자. x,y는 모두 PositionXY에 잘 들어갔는데, z는 PositionY 쪽이 조금 더 넓은 타입이다.

이런 경우 PositionX의 z와 PositionY의 z의 공통 분모인 number 타입만 할당이 가능하게 된다.

// 객체끼리 합칠 때 한 속성의 키값은 겹치는데 밸류값이 다른 경우
type PositionX = { x: number; z: number };
type PositionY = { y: number; z: number | string };
type PositionXY = PositionX & PositionY;

const positionXY1: PositionXY = { x: 1, y: 1, z: "a" }; // 에러: z가 불가능. 
const positionXY2: PositionXY = { x: 1, y: 1, z: 1 }; // 가능.
// z에는 PositionX, PositionY 모두 교집합인 number타입이 할당되기 때문이다.

 

만약 PositionX의 z가 number고 PositionY의 z가 string이면? 즉, 키값이 하나도 안 겹친다면 어떻게 될까?

정답은 never가 나온다.

// 객체끼리 합칠 때 한 속성의 키값은 겹치는데 밸류값이 다른 경우
type PositionX = { x: number; z: number };
type PositionY = { y: number; z: string };
type PositionXY = PositionX & PositionY;

const PositionedXY: PositionXY = { x: 1, y: 1, z: "a" }; // z는 never
// z는 never이며, z를 아예 뺄수도 없다. z를 빼면 PositionXY라는 변수이름에 에러가 뜬다.
// 즉 그냥 못 쓰게 된다😅

 

이렇게 둘이 합치고 싶은데 z같은 녀석들은 제외하고 싶다면 Pick이나 Omit을 사용해서 제대로 걸러야 하겠다.

 

4.  인터페이스들끼리 조합할 때

인터페이스는 기본적으로 객체형이다. 그렇다면 어떻게 &가 작용할지 대충 감이 온다.

두 인터페이스를 합치기 위해 interface를 쓸 수는 없다. 오로지 인터섹션 타입은 type선언자를 써야한다.

그리고 공통 분모가 없는 값들을 인터섹션 타입으로 묶었을 때 깔끔히 합집합처럼 묶이는걸 볼 수 있다.

interface InterA {
  x: number,
  y: number,
}

interface InterB {
  y: number,
  z: number,
}

type InterC = InterA & InterB

const interC: InterC = {
  x: 1,
  y: 2,
  z: 3,
}

 

그러나 여기서도 똑같다. InterA와 InterB에서 겹치는 키값이 있는데 값이 아예 다르다면 묶었을 때 never가 나온다. 

interface InterA {
  x: number,
  z: string,
}

interface InterB {
  z: number,
  u: number,
}

type InterC = InterA & InterB

const interC: InterC = {
  x: 1,
  y: 2,
  z: 3, // z는 never여서 에러표시 뜸.
  u: 4
}

 

 

마무리

자 이렇게 오해도 많고 사용할 때마다 탈도 많은 인터섹션 타입을 정리해보았다.

되게 많은 타입을 &연산자 하나로 묶을 수 있고, 그 작용방식이 딱 한가지가 아니라 오해가 많은거 같다.

이럴때는 실습이 최고라는걸 다시 한 번 느낀다.