유니온과 인터섹션 — A | B 와 A & B
"이것 또는 저것" 과 "이것이면서 저것" — 가장 자주 쓰는 두 합성.
입문 파트가 끝났습니다. 5편부터는 기초 파트 — 본격적으로 "TS 다운" 타입 합성을 다룹니다. 그 첫 번째가 유니온(|)과 인터섹션(&)입니다. 둘은 거의 모든 실전 타입 설계의 출발점이에요.
유니온 — "이것 또는 저것"
// 변수가 string 또는 number 중 하나
let id: string | number;
id = "abc"; // OK
id = 42; // OK
id = true; // ❌
// 함수 매개변수도 유니온
function format(input: string | number): string {
// 여기서는 input 이 string|number — 양쪽 다에 있는 메서드만 호출 가능
return String(input);
}
유니온은 "가능한 타입의 집합"입니다. 사용하려면 "지금 무엇인지" 좁혀야 합니다 — 그게 다음 절의 narrowing.
좁히기(narrowing) — 유니온을 다루는 핵심
function format(input: string | number): string {
if (typeof input === "string") {
return input.toUpperCase(); // ✅ 여기서는 string 확정
}
return input.toFixed(2); // ✅ 여기서는 number 확정
}
TS 는 typeof·instanceof·in·=== 같은 검사를 보면 흐름에 따라 타입을 좁힙니다. 이걸 제어 흐름 분석(control flow analysis) 이라고 부르는데, IDE 자동완성이 if 안과 밖에서 달라지는 비결입니다.
| 좁히는 도구 | 예시 | 언제 |
|---|---|---|
| typeof | typeof x === "string" | primitive |
| instanceof | err instanceof Error | 클래스 인스턴스 |
| in | "name" in obj | 객체 속성 유무 |
| === | shape.kind === "circle" | 리터럴 비교 (구분 가능 유니온) |
| 사용자 정의 | function isUser(x): x is User | 14편 |
구분 가능 유니온 — 실전의 보석
// 각 멤버에 "꼬리표"(discriminant) 가 있는 유니온
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2; // s 는 circle 로 좁혀짐
case "square": return s.side ** 2;
case "rect": return s.w * s.h;
}
}
실전에서 가장 많이 쓰는 패턴. API 응답의 success/error 분기, 리덕스 액션, 상태 머신 — 거의 모두 구분 가능 유니온으로 모델링합니다. kind / type / status 같은 literal 필드 하나로 나머지 모양이 결정되니까요.
인터섹션 — "이것이면서 저것"
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // { name; age }
const p: Person = { name: "준성", age: 39 };
// 함수 합치기 — mixin 패턴
type Loggable = { log(): void };
type Serializable = { toJSON(): string };
type Service = Loggable & Serializable;
// 함수 시그니처 합성
type Add = (a: number, b: number) => number;
type Bind = (this: void) => void;
type API = Add & Bind; // 같은 함수가 두 호출 시그니처를 다 갖는다
인터섹션은 "두 모양을 모두 만족" 입니다. 객체 타입을 결합할 때 가장 자주 쓰고, 인터페이스의 extends 와 거의 같은 역할을 합니다.
유니온 vs 인터섹션 — 직관과 반대인 한 가지
"| 는 OR 라서 더 넓다? 아니요." 타입에서는 거꾸로입니다.
· A | B — 값의 집합은 더 넓지만, 사용할 수 있는 속성은 더 좁다(양쪽에 공통인 것만).
· A & B — 값의 집합은 더 좁지만, 사용할 수 있는 속성은 더 넓다(양쪽 합).
type Dog = { bark(): void };
type Cat = { meow(): void };
let pet: Dog | Cat = { bark(){} };
pet.bark(); // ❌ 'Cat' 에는 bark 가 없으니까 안전하게 막힘
pet.meow(); // ❌
let monster: Dog & Cat = { bark(){}, meow(){} };
monster.bark(); // ✅
monster.meow(); // ✅
리터럴 유니온 — enum 의 대체
// 7편에서 다룰 enum 의 가벼운 대안
type Theme = "light" | "dark" | "auto";
type Status = 200 | 404 | 500;
function setTheme(t: Theme) { ... }
setTheme("light"); // OK
setTheme("blue"); // ❌ 컴파일 시점에 차단
대부분의 상태값·테마·HTTP 코드 등은 enum 보다 리터럴 유니온이 가볍고 다루기 좋습니다. 7편에서 enum 과 비교.
6편 — 제네릭 입문
<T> 한 글자의 힘. 왜 제네릭이 라이브러리 코드의 핵심인지.
이전: 4편 interface vs type · 현재: 5편 (기초 시작) · 다음 → 6편 제네릭 · 진행: 5/20