개발새발

타입을 집합으로 생각하자 (유니언, 인터섹션) 본문

Typescript

타입을 집합으로 생각하자 (유니언, 인터섹션)

비숑주인 2025. 3. 22. 16:40

TypeScript는 타입을 집합(set) 처럼 다룬다. 집합 이론의 관점에서 타입 연산자를 해석하면, 타입 시스템을 더 명확하게 이해할 수 있고, 복잡한 타입도 직관적으로 설계할 수 있다. 

 

이번 글에서는 | (유니언), & (인터섹션), 그리고 unknown, never 등의 특수 타입을 집합 개념으로 설명하겠다. 

 

유니언 타입: |는 합집합

 

유니언(union)은 둘 중 하나라도 만족하는 값을 의미한다. 수학적 개념으로는 합집합이다. 

let strOrNum: string | number = 'hello';
strOrNum = 123;

 

타입 string | number는 문자열이거나 숫자인 값을 모두 포함하는 집합이다. 

 

 

인터섹션 타입: &는 교집합

 

인터섹션(intersection)은 두 타입을 모두 만족하는 값을 의미한다. 수학적으로는 교집합이다. 

 

type NeverType = string & number; // ❌

 

여기서 string이면서 동시에 number인 값은 존재할 수 없다. 따라서 이 타입은 never, 즉 공집합이다. 

+ never는 값이 절대 존재할 수 없는 타입이다! 

 

타입의 전체집합과 공집합

 

개념 TypeScript 타입 의미
전체집합 unknown 모든 타입의 최상위 타입
공집합 never 어떤 값도 가질 수 없는 타입
const a: unknown = 123;    // ✅ 모든 타입 가능
const b: never = 123;      // ❌ Error

 

  • unknown은 어떤 값도 받을 수 있다.
  • never는 어떤 값도 가질 수 없다.

 

대입 관계 (서브타입 관계)

항상 좁은 타입은 넓은 타입에 대입 가능하지만, 그 반대는 안 된다. 

 

let a: unknown;
a = 'hello';      // ✅ string → unknown

let b: string;
// b = a;         // ❌ unknown → string (안전하지 않음)

 

  • never ⊆ 모든 타입
  • 모든 타입 ⊆ unknown
  • 따라서: never → 모든 타입 → unknown 방향으로만 대입 가능

 

유니언 & 인터섹션 예제 정리

type A = string | boolean;          // string 또는 boolean
type B = boolean | number;          // boolean 또는 number
type C = A & B;                     // A와 B의 교집합 → boolean

type D = {} & (string | null);      // string
type E = string & boolean;          // never
type F = unknown | {};              // unknown
type G = never & {};                // never

 

 

  • C: A와 B의 공통 타입은 boolean
  • D: {}는 null/undefined를 제외한 값이므로 string과 교집합 가능
  • E: string과 boolean은 공통 값이 없으므로 never
  • F: unknown과 어떤 타입의 합집합은 항상 unknown
  • G: 공집합과의 교집합은 항상 never

 

예외적인 교집합 – 객체와 원시값

일반적으로 객체와 원시값의 교집합은 never 여야 하지만, 예외도 있다. 

type H = { a: 'b' } & number;   // { a: 'b' } & number
type I = null & { a: 'b' };     // never
type J = {} & string;           // string

 

주의할 점

  • H는 never가 아닙니다. TypeScript에서는 브랜딩 기법을 위해 이런 형태를 허용한다.
  • J가 string인 이유는 {}는 null/undefined를 제외한 모든 값을 포함하기 때문이다.  

 

실무에서의 활용 팁

 

유니언으로 여러 가능성 명시

type ResponseStatus = 'success' | 'fail' | 'loading';

 

✅ 인터섹션으로 여러 타입 결합

 

type WithID = { id: string };
type WithTimestamp = { createdAt: Date };

type Entity = WithID & WithTimestamp;

const user: Entity = {
  id: 'abc',
  createdAt: new Date(),
};

 

✅ 조건부 타입과 함께 사용

 

type Flatten<T> = T extends any[] ? T[number] : T;

type A = Flatten<string[]>;   // string
type B = Flatten<number>;     // number

 

 

연산자 / 키워드 의미 예시 결과
` ` 합집합 (or)
& 교집합 (and) string & boolean → never
never 공집합 값 없음
unknown 전체집합 모든 타입 포함
any 특수 케이스 일관성 없음, 지양