유니온과 인터섹션 타입
여러 후보 중 하나를 허용하는 유니온과 모든 구조를 결합하는 인터섹션을 구분해 API 응답과 기능 조합을 모델링합니다.
프로그래밍에서는 하나의 변수가 여러 타입 중 어떤 하나가 될 수도 있고, 여러 타입의 특징을 모두 가져야 할 때도 있습니다.
타입스크립트는 이러한 복잡한 시나리오를 효과적으로 다룰 수 있도록 유니온(Union) 타입과 인터섹션(Intersection) 타입이라는 강력한 도구를 제공합니다.
이 절에서는 이 두 가지 특수 타입이 무엇이며, 어떻게 활용되는지 자세히 살펴보겠습니다.
TYPE / RUNTIME / CONTRACT
유니온은 후보, 인터섹션은 동시 요구를 합친다
두 타입의 비교에는 시간 순서가 없습니다. 허용할 값과 사용 가능한 연산, 객체를 만들 때 충족할 요구를 따로 읽습니다. & 연산이 런타임 객체를 병합하지는 않습니다.
UNION / VALUES
UserId = string | number
"abc123"과456789를 모두 허용하고 boolean은 거절합니다. 값을 읽을 때 모든 후보에서 유효한 연산만 바로 사용할 수 있습니다.
INTERSECTION / REQUIREMENTS
Manager = Employee & Person
employeeId/department/name/age를 모두 갖춰야 합니다. 정적 요구를 결합할 뿐 누락된 속성이나 메서드를 런타임에 만들어 주지 않습니다.
UNION / USE
typeof 뒤 문자열과 숫자의 연산 분리
본문 printId는 string이면 toUpperCase, number이면 toFixed(2)를 호출합니다. "uniqueUser123"과789.123의 결과는 각각 대문자 ID와789.12입니다.
INTERSECTION / CONSTRUCT
SuperUser의 데이터와 관리 메서드
User의 id/email과 AdminPrivileges의 두 메서드를 실제 값에 제공합니다. 별칭 선언만으로 관리 권한을 검사하거나 메서드 구현을 생성하지 않습니다.
INTERSECTION / CONFLICT
공통 값이 없으면 never가 될 수 있다
string & number는 never지만 string & "ready"는 "ready"입니다. 판별 속성의 충돌은 객체 전체를 never로 줄일 수 있습니다. 모든 같은 이름 속성의 결합이 실패하는 것은 아닙니다.
STATIC / RUNTIME
응답 선언과 입력 검증은 다른 책임
본문 ApiResponse는 success/error를 status로 좁히지만 data:any를 사용하고 validator와 never 검사는 없습니다. 다음 그림의 확장 설계를 실제 코드 구현으로 해석하지 않습니다.
유니온은 허용할 후보를 넓히고 인터섹션은 동시 요구를 적용합니다. 값의 생성·사용·충돌을 구분하며 두 연산을 런타임 객체 병합으로 해석하지 않습니다.
유니온 타입 (Union Types)
유니온 타입은 변수나 매개변수가 여러 타입 중 하나를 가질 수 있도록 정의할 때 사용합니다.
파이프(|) 기호를 사용하여 여러 타입을 연결하며, "A 이거나 B 이다"라는 의미를 가집니다.
이는 특정 값이 다양한 형태를 가질 수 있는 유연성을 제공하면서도, 여전히 타입 안정성을 유지할 수 있도록 돕습니다.
예를 들어 웹사이트에서 사용자 ID가 숫자일 수도 있고, 문자열일 수도 있다고 가정해봅시다.
// string 또는 number 타입이 될 수 있는 UserId 타입을 정의합니다.
type UserId = string | number;
let user1Id: UserId = "abc123"; // 문자열 할당 가능
let user2Id: UserId = 456789; // 숫자 할당 가능
// 오류: UserId 타입은 boolean을 허용하지 않습니다.
// let user3Id: UserId = true; // Error: Type 'boolean' is not assignable to type 'UserId'.
function printId(id: UserId) {
console.log(`The ID is: ${id}`);
// 유니온 타입의 값에 접근할 때는, 해당 타입들의 공통 속성만 직접 접근 가능합니다.
// console.log(id.toUpperCase()); // Error: 'id'는 'string | number' 타입이므로 'toUpperCase' 속성이 없습니다.
// number 타입에는 toUpperCase()가 없기 때문입니다.
// 타입을 좁히는(Narrowing) 과정을 통해 특정 타입의 속성에 접근할 수 있습니다.
if (typeof id === "string") {
// 이 블록 안에서 id는 string 타입으로 추론됩니다.
console.log(`ID in uppercase: ${id.toUpperCase()}`);
} else {
// 이 블록 안에서 id는 number 타입으로 추론됩니다.
console.log(`ID fixed to 2 decimal places: ${id.toFixed(2)}`);
}
}
printId("uniqueUser123"); // The ID is: uniqueUser123, ID in uppercase: UNIQUEUSER123
printId(789.123); // The ID is: 789.123, ID fixed to 2 decimal places: 789.12유연성: 하나의 변수가 여러 가지 가능한 타입을 가질 수 있습니다.
안전성: 유니온 타입의 변수에 접근할 때는 모든 유니온 멤버 타입에 공통적으로 존재하는 속성/메서드에만 직접 접근할 수 있습니다.
속성 이름이 양쪽에 있더라도 연산이 모든 후보에서 유효해야 합니다. 예를 들어 같은 이름의 함수 속성에 서로 다른 매개변수 제약이 있으면 이름만 보고 임의의 인자로 호출할 수 없습니다.
예를 들어 string | number 타입 변수에는 length 속성이나 toFixed() 메서드에 직접 접근할 수 없습니다.
타입 좁히기(Narrowing): typeof, instanceof, in 연산자 또는 사용자 정의 타입 가드(Type Guards)와 같은 방법을 사용하여 코드 블록 내에서 변수의 타입을 특정 타입으로 좁힐(Narrow) 수 있습니다.
이렇게 타입을 좁히고 나면 해당 타입에 특화된 속성이나 메서드를 안전하게 사용할 수 있습니다.
인터섹션 타입 (Intersection Types)
인터섹션 타입은 여러 타입의 요구를 동시에 만족하는 값을 표현할 때 사용합니다. 객체 타입에서는 양쪽의 속성 계약을 함께 요구하지만, &가 실행 중 객체를 합치거나 누락된 값을 만들어 주는 것은 아닙니다.
앰퍼샌드(&) 기호를 사용하여 여러 타입을 연결하며, "A 이고 B 이다" 또는 "A와 B의 모든 특징을 가진다"라는 의미를 가집니다.
이는 여러 인터페이스나 객체 타입의 속성들을 하나로 합쳐서 더 풍부한 단일 타입을 정의할 때 매우 유용합니다.
// Employee 인터페이스 정의
interface Employee {
employeeId: string;
department: string;
}
// Person 인터페이스 정의 (2장 3절에서 사용했던 것과 유사)
interface Person {
name: string;
age: number;
}
// Employee와 Person 인터페이스의 모든 속성을 가진 Manager 타입을 정의합니다.
type Manager = Employee & Person;
let manager1: Manager = {
employeeId: "MGR001",
department: "Sales",
name: "이지혜",
age: 45,
};
console.log(manager1.name); // 이지혜
console.log(manager1.employeeId); // MGR001
// 오류: Manager 타입이 요구하는 모든 속성을 포함해야 합니다.
// let incompleteManager: Manager = {
// name: "김철수",
// age: 30
// }; // Error: Property 'employeeId' is missing in type '{ name: string; age: number; }' but required in type 'Manager'.결합: 여러 타입이 가진 모든 멤버(속성, 메서드)를 새로운 하나의 타입으로 합칩니다.
강화: 기존 타입에 새로운 속성을 추가하는 것처럼 활용할 수 있습니다.
예를 들어, BaseType & { newProp: string }와 같이 기존 타입에 newProp 속성을 추가한 새로운 타입을 만들 수 있습니다.
충돌 처리: 같은 속성은 양쪽 타입을 동시에 만족해야 합니다. 공통 값이 없는 string & number는 never가 되지만 string & "ready"는 "ready"가 됩니다. 판별 속성의 충돌은 객체 전체가 never로 축소될 수도 있습니다.
유니온과 인터섹션 타입의 활용 예시
두 타입은 실제 애플리케이션 개발에서 매우 자주 사용됩니다.
예시 1: API 응답 처리 (유니온)
서버로부터 받아오는 데이터가 성공 응답일 수도 있고, 오류 응답일 수도 있다고 가정해봅시다.
interface SuccessResponse {
status: "success";
data: any; // 실제 데이터 타입은 더 구체적으로 정의할 수 있습니다.
}
interface ErrorResponse {
status: "error";
message: string;
code: number;
}
// API 응답은 성공 응답이거나 오류 응답 중 하나입니다.
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
if (response.status === "success") {
// 여기서는 response가 SuccessResponse 타입으로 좁혀집니다.
console.log("데이터를 성공적으로 받았습니다.", response.data);
} else {
// 여기서는 response가 ErrorResponse 타입으로 좁혀집니다.
console.log(`오류 발생 (${response.code}): ${response.message}`);
}
}
// 함수 호출 예시
handleResponse({ status: "success", data: { user: "Alice" } });
handleResponse({ status: "error", message: "인증 실패", code: 401 });이처럼 status와 같은 식별 가능한 속성(Discriminant Property)으로 유니온을 좁힐 수 있습니다. 위 코드는 success·error 두 상태를 처리하며 입력 검증과 never 검사는 구현하지 않았습니다.
다음 그림은 여기에 로딩 상태를 더해 loading·success·failure로 나누고, 외부 입력 검증과 누락 검사를 추가한 확장 설계입니다.
검증된 결과와 완전한 상태 처리 — 상태 추적
별도 parseLoadResult/describeResult 실제 확장 코드가 있어 unknown 입력 검증·거절 또는 세 상태의 분기에서 현재 값과 처리 결과를 추적할 수 있다. 상태 이름을 success/error 예제와 섞지 않는다.
TYPE / RUNTIME / CONTRACT
검증과 판별 상태 처리, 누락 검사를 연결한다
본문의 success/error 예제를 loading/success/failure로 확장한 설계입니다. 외부 입력은 검증을 먼저 거치고, 상태별 처리 뒤 남은 경우가 없는지 정적으로 확인합니다.
EXTENSION / VALIDATE
외부 입력에서 상태 필드를 확인
타입 주석으로 입력을 검증하지 않습니다. status와 각 상태에 필요한 필드, 값 조건을 확인해 내부 유니온으로 변환합니다.
EXTENSION / STATE
상태별 필드만 사용하는 분기
새 확장 코드의 loading은 로딩 안내를, success는 data를, failure는 message를 사용합니다. switch나 상태 비교로 좁힌 범위에서 해당 필드를 사용합니다.
EXTENSION / EXHAUSTIVE
남은 값의 never 할당으로 누락 발견
모든 상태를 처리한 나머지를 never에 할당합니다. 새 상태를 추가하고 처리하지 않으면 진단됩니다. 입력 validator의 정확성을 이 검사 하나로 보장할 수는 없습니다.
ACTUAL LESSON / TWO STATES
원 예제와 확장 코드를 구분
원 handleResponse는 success/error를 비교해 data 또는 message/code를 읽습니다. 이어지는 확장 실습은 parseLoadResult에서 unknown을 검증하고 describeResult에서 loading/success/failure와 switch/never 검사를 실제로 구현합니다.
입력 검증 뒤 내부 상태를 분기하고 모든 경우의 처리를 확인합니다. 본문의 success·error 코드와 loading·failure·validator·never를 추가한 확장 설계를 구분합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 원래 본문의 구현과 도표가 설명한 설계 보강을 분리한다. 서사에 포함한 확장 동작은 assumptions에 명시하고 이미 구현된 안전장치로 주장하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | unknown→런타임 검증의 통과·거절 분기와, 통과한 유니온의 loading/success/failure 분기다. 노드 8개·간선 7개가 데스크톱과 모바일에서 같다. 검증 거절은 끝나며 상태 분기로 연결되지 않는다. 기존 handleResponse의 두 상태와 본문의 새 parseLoadResult/describeResult의 세 상태를 구분한다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 외부 입력 | 입력: unknown / 기존 2상태: 이 확장과 별개 | unknown 값을 parseLoadResult에 전달한다 | loading·success·failure를 사용하는 확장 예제를 명시한다 |
| 검증과 tag | 거절: 내부 상태 처리 없음 / 통과: LoadResult | 검증에서 거절되면 끝내고, 통과한 값의 status를 확인한다 | type assertion만으로 runtime 검증을 대신하지 않는다 |
| 완전한 처리 | 결과: 해당 tag 표현 / 누락: 정적 never 진단 | loading 안내, success data, failure message 중 하나를 고르고 누락은 never로 진단한다 | 세 표현이 순차 실행되는 시간선은 아니다 |
검증된 결과와 완전한 상태 처리 — 상태 추적
별도 parseLoadResult/describeResult 실제 확장 코드가 있어 unknown 입력 검증·거절 또는 세 상태의 분기에서 현재 값과 처리 결과를 추적할 수 있다. 상태 이름을 success/error 예제와 섞지 않는다.
TYPE / RUNTIME / CONTRACT
검증과 판별 상태 처리, 누락 검사를 연결한다
본문의 success/error 예제를 loading/success/failure로 확장한 설계입니다. 외부 입력은 검증을 먼저 거치고, 상태별 처리 뒤 남은 경우가 없는지 정적으로 확인합니다.
EXTENSION / VALIDATE
외부 입력에서 상태 필드를 확인
타입 주석으로 입력을 검증하지 않습니다. status와 각 상태에 필요한 필드, 값 조건을 확인해 내부 유니온으로 변환합니다.
EXTENSION / STATE
상태별 필드만 사용하는 분기
새 확장 코드의 loading은 로딩 안내를, success는 data를, failure는 message를 사용합니다. switch나 상태 비교로 좁힌 범위에서 해당 필드를 사용합니다.
EXTENSION / EXHAUSTIVE
남은 값의 never 할당으로 누락 발견
모든 상태를 처리한 나머지를 never에 할당합니다. 새 상태를 추가하고 처리하지 않으면 진단됩니다. 입력 validator의 정확성을 이 검사 하나로 보장할 수는 없습니다.
ACTUAL LESSON / TWO STATES
원 예제와 확장 코드를 구분
원 handleResponse는 success/error를 비교해 data 또는 message/code를 읽습니다. 이어지는 확장 실습은 parseLoadResult에서 unknown을 검증하고 describeResult에서 loading/success/failure와 switch/never 검사를 실제로 구현합니다.
입력 검증 뒤 내부 상태를 분기하고 모든 경우의 처리를 확인합니다. 본문의 success·error 코드와 loading·failure·validator·never를 추가한 확장 설계를 구분합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 원래 본문의 구현과 도표가 설명한 설계 보강을 분리한다. 서사에 포함한 확장 동작은 assumptions에 명시하고 이미 구현된 안전장치로 주장하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | unknown→런타임 검증의 통과·거절 분기와, 통과한 유니온의 loading/success/failure 분기다. 노드 8개·간선 7개가 데스크톱과 모바일에서 같다. 검증 거절은 끝나며 상태 분기로 연결되지 않는다. 기존 handleResponse의 두 상태와 본문의 새 parseLoadResult/describeResult의 세 상태를 구분한다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 외부 입력 | 입력: unknown / 기존 2상태: 이 확장과 별개 | unknown 값을 parseLoadResult에 전달한다 | loading·success·failure를 사용하는 확장 예제를 명시한다 |
| 검증과 tag | 거절: 내부 상태 처리 없음 / 통과: LoadResult | 검증에서 거절되면 끝내고, 통과한 값의 status를 확인한다 | type assertion만으로 runtime 검증을 대신하지 않는다 |
| 완전한 처리 | 결과: 해당 tag 표현 / 누락: 정적 never 진단 | loading 안내, success data, failure message 중 하나를 고르고 누락은 never로 진단한다 | 세 표현이 순차 실행되는 시간선은 아니다 |
확장 설계를 코드로 따라가기
위 두 상태 예제와 별도로, 다음 파일은 그림의 세 상태와 입력 검증·누락 검사를 실제로 연결합니다. strict: true, target: "ES2020"에서 별도 파일로 실행하세요. 두 예제의 error와 failure 이름을 한 타입에 섞지 않습니다.
type LoadResult =
| { status: "loading" }
| { status: "success"; data: number }
| { status: "failure"; message: string };
function parseLoadResult(input: unknown): LoadResult | undefined {
if (typeof input !== "object" || input === null || !("status" in input)) {
return undefined;
}
if (input.status === "loading") return { status: "loading" };
if (input.status === "success" && "data" in input &&
typeof input.data === "number" && Number.isFinite(input.data)) {
return { status: "success", data: input.data };
}
if (input.status === "failure" && "message" in input &&
typeof input.message === "string") {
return { status: "failure", message: input.message };
}
return undefined;
}
function assertNever(value: never): never {
throw new Error("처리하지 않은 상태");
}
function describeResult(result: LoadResult): string {
switch (result.status) {
case "loading": return "로딩 중";
case "success": return `점수 ${result.data.toFixed(1)}`;
case "failure": return `실패: ${result.message}`;
default: return assertNever(result);
}
}
for (const input of [
{ status: "success", data: 3 },
{ status: "failure", message: "응답 없음" },
{ status: "success", data: "3" },
]) {
const result = parseLoadResult(input);
console.log(result === undefined ? "입력 거절" : describeResult(result));
}출력은 차례로 점수 3.0, 실패: 응답 없음, 입력 거절입니다. 먼저 객체·속성·값을 확인해 새 계약 값을 만들고, 검증된 값의 status로 필요한 속성을 좁힙니다. 마지막 default는 지금의 세 상태를 처리하고 나면 never여야 합니다. 이 검사는 외부 값의 유효성 검사를 대신하지 않습니다. 그림의 검증과 분기는 이 두 경계를 분리해 읽습니다.
독립 변형: 입력에 { status: "loading" }을 추가하면 어떤 문구가 나오나요? 이어서 LoadResult에 { status: "cancelled" } 후보만 추가하고 describeResult를 그대로 두면 어디에서 진단될까요? 마지막으로 취소 상태를 지원하려면 입력 검증과 표시 코드 각각 어느 분기를 추가해야 하는지 적어보세요.
새 입력과 누락 검사 확인
로딩 입력은 로딩 중을 출력합니다. 타입에만 취소 후보를 추가하면 default의 assertNever(result)에서 취소 후보를 never로 넘길 수 없다고 진단합니다. parseLoadResult의 취소 상태 검증·반환 분기와 describeResult의 case "cancelled" 표시 분기를 함께 추가해야 외부 취소 입력도 처리합니다. 타입 선언만 추가해도 현재 검증 함수가 취소 입력을 자동 승인하는 것은 아닙니다.
공식 누락 검사 설명은 새로운 유니온 멤버를 추가했을 때 남는 후보를 never 검사로 찾는 원리를 설명합니다.
예시 2: 기능 결합 (인터섹션)
특정 사용자가 일반 사용자 기능과 관리자 기능을 모두 가지고 있다고 정의하고 싶을 때.
interface User {
id: string;
email: string;
}
interface AdminPrivileges {
manageUsers: () => void;
deleteContent: () => void;
}
// User의 속성과 AdminPrivileges의 속성을 모두 가진 슈퍼 유저
type SuperUser = User & AdminPrivileges;
let superAdmin: SuperUser = {
id: "super_admin_001",
email: "[email protected]",
manageUsers: () => console.log("사용자를 관리합니다."),
deleteContent: () => console.log("콘텐츠를 삭제합니다."),
};
console.log(superAdmin.email);
superAdmin.manageUsers();SuperUser 타입은 User 타입의 id, email 속성과 AdminPrivileges 타입의 manageUsers, deleteContent 메서드를 모두 포함하게 됩니다.
유니온과 인터섹션은 둘 다 타입을 조합하지만, 의미는 정반대입니다.
유니온 타입과 인터섹션 타입은 타입스크립트의 유연성과 표현력을 크게 향상시키는 중요한 개념입니다.
유니온은 이것 또는 저것의 관계를, 인터섹션은 이것과 저것 모두의 관계를 정의할 때 사용된다는 점을 명확히 이해하는 것이 중요합니다.
이 두 가지를 적절히 활용하면 더욱 강력하고 안정적인 타입을 설계할 수 있습니다.