[TypeScript] Union Type과 Intersection Type

기운찬곰·2023년 3월 25일
0

TypeScript

목록 보기
1/1

Overview

요즘에 이펙티브 타입스크립트 책을 읽고 있습니다. 저 스스로 한 달에 한 번 이 달의 개발도서를 선정해서 읽고 있는데요. 나름 재미있고 뿌듯하답니다. 다만, 완독을 할 때보다 하지 못할 때가 더 많긴 하지만 그래도 목표가 없는 것 보다는 나은거 같습니다.

이펙티브 타입스크립트 책을 읽다보면 알고 있는 개념도 있고 제대로 몰랐던 개념도 있는 거 같습니다. 특히 오늘 소개하는 내용은 그 중에서 좀 혼란스러웠던 개념에 대해 정리한 글입니다. (아이템 7장. 타입이 값들의 집합이라고 생각하기 편입니다)


타입을 집합의 관점에서 생각하기

타입을 집합의 관점에서 생각을 하면 타입의 의미가 딱 맞아떨어지더군요. 간단한 예시를 볼까요?

type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;

const ab: AB = Math.random() < 0.5 ? 'A' : 'B'; 
const ab12: AB12 = ab; // 정상

ab는 'A' 또는 'B'일 수도 있습니다. 타입 AB12는 그보다 더 포괄적인 범위인 'A' | 'B' | 12 로 되어있습니다. 따라서 집합의 관점에서 생각하자면 해당 코드는 정상적으로 동작합니다.

여기까지는 크게 어렵지 않았습니다. 하지만 다음 내용을 볼까요?


인터페이스 intersection(교집합) 생각해보기

& 연산자는 두 타입의 인터섹션(intersection, 교집합)을 계산합니다. 아래 코드를 한 번 볼까요?

interface Person {
  name: string;
}

interface Lifespan {
  birth: Date;
  death?: Date;
}

type PersonSpan = Person & Lifespan;

언뜻보면 PersonSpan 타입은 Person과 Lifespan의 공통 속성이 없기 때문에 교집합이 없어서 공집합(never)이 아닐까 생각하기 쉽습니다.

그러나 타입 연산자는 인터페이스의 속성이 아닌, 값의 집합에 적용됩니다. 🤔

저 말의 의미를 생각해보면 {name, birth} 를 가지고 있는 값은 Person도 속하고, Lifespan도 속하니까 교집합이 성립한다는 거네요. 만약 {name} 만 있으면 이건 Person은 성립하는데 Lifespan은 성립하지 않으니까 안되는거고요.

const ps: PersonSpan = { // 정상
  name: 'Alan',
  birth: new Date('1912/06/23')
}

// Property 'birth' is missing in type '{ name: string; }' but required in type 'Lifespan'.
const ps: PersonSpan = { 
  name: 'Alan',
}

✍️ 인터섹션 타입의 값은 각 타입 내의 속성을 모두 포함하는 것이 일반적인 규칙이다.


인터페이스 union(합집합) 생각해보기

이번에는 인터페이스 유니온(union, 합집합)에 대해 생각해봅시다. 생각해보면 유니온은 Person만 포함하면 되고, Lifespan만 포함해도 됩니다. 즉, {name} 혹은 {birth}, {name, birth} 상관 없습니다.

interface Person {
  name: string;
}

interface Lifespan {
  birth: Date;
  death?: Date;
}

type PersonSpan = Person | Lifespan;

const ps: PersonSpan = { // 정상
  birth: new Date()
}

하지만 아래 경우를 생각해볼까요? 이번에는 이상하게도 name만 되고, age, skill은 안되는 것을 알 수 있습니다. 도대체 왜 그런걸까요? 🤔

// 참고 : https://velog.io/@soulee__/TypeScript-Union-Type

interface Person {
  name: string;
  age: number;
}
interface Developer {
  name: string;
  skill: string;
}
function introduce(someone: Person | Developer) {
  someone.name; // O 정상 동작
  someone.age; // X 타입 오류
  someone.skill; // X 타입 오류
}

타입스크립트 관점에서 introduce 함수를 호출하는 시점에 Person 타입이 올지, Developer 타입이 올 지 알 수 없기 때문에 어느 타입이 들어오든 간에 오류가 안 나는 방향으로 타입을 추론하게 된다고 합니다. (!!)

그래서 name만 확실히 가능하니까 되는 거고, 나머지에 대해서는 안 되는 것이었네요.

만약 확실히 Person 이거나 Developer인 상황에서는 {name, age}도 될 것이고, {name, skill}도 될 것입니다. 오히려 이 경우에는 name만 있을 때 에러가 발생합니다.

type PersonDeveloper = Person | Developer

const person: PersonDeveloper = {
	name: 'person a',
	age: 12
}

keyof - union, intersection 알아보기

이번에는 타입 자체에 union과 intersection을 한 결과를 keyof로 반환받으면 어떻게 될 지 생각해보겠습니다.

type a = keyof (Person | Developer) // "name"
type b = keyof (Person & Developer) // "name" | "age" | "skill"
  • union 했을 때 결과를 보면 "name" 입니다. 단순히 union 의미만 생각했을 때는 "name" | "age" | "skill"이 맞지 않을까 생각했었는데 틀렸습니다. 이는 데이터가 하나도 할당되지 않은 상태에서 keyof Union을 하면 확실히 결정할 수 있는 타입은 "name" 하나 밖에 없기 때문입니다.

  • intersection 했을 때 결과를 보면 "name" | "age" | "skill" 입니다. 역시 단순히 교집합의 의미만 놓고 받을 때 "name"이지 않을까 생각합니다. 하지만 Person & Developer 을 하면 { name, age, skill } 이여야 되기 때문에 "name" | "age" | "skill"가 맞습니다.

이를 정리해보면 다음과 같습니다. 🤣

keyof (A & B) = (keyof A) | (keyof B)
keyof (A | B) = (keyof A) & (keyof B)

마치면서

타입스크립트에 대해 어느정도 알고 있다고 생각했는데 다시 봐도 좀 어렵고 혼란스러운 부분이 있는 거 같습니다. 좀 더 분발해야겠습니다.

profile
배움을 좋아합니다. 새로운 것을 좋아합니다.

0개의 댓글