대수적 데이터 타입: 합 타입과 곱 타입
대수적 데이터 타입(Algebraic Data Type, ADT)은 타입을 조합해서 새 타입을 만드는 방법입니다. 대수(algebra)라는 이름처럼 타입들 사이에 “덧셈”과 “곱셈”에 해당하는 두 가지 조합 방식이 있습니다.
곱 타입 (Product Type)
두 타입 A, B의 모든 조합을 가진 타입입니다. 가능한 값의 수가 |A| × |B|이기 때문에 곱 타입입니다.
// 튜플 — 가장 단순한 곱 타입
type Point = [number, number]; // number × number
// 객체/레코드
type User = {
name: string; // 가능한 string 수
age: number; // × 가능한 number 수
active: boolean; // × 2 (true | false)
};
// 가능한 User의 수 = string 수 × number 수 × 2곱 타입은 모든 필드가 동시에 존재합니다. User는 name, age, active를 항상 동시에 가집니다.
합 타입 (Sum Type / Union Type)
두 타입 A, B 중 하나인 타입입니다. 가능한 값의 수가 |A| + |B|이기 때문에 합 타입입니다.
// TypeScript의 union type이 합 타입
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
// Shape는 세 가지 형태 중 하나합 타입은 하나의 값이 여러 경우 중 하나임을 표현합니다. 동시에 circle이면서 rectangle인 Shape는 없습니다.
패턴 매칭으로 합 타입 처리
합 타입은 모든 경우를 처리했는지 컴파일러가 확인할 수 있게 해줍니다.
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
// 모든 경우를 처리하지 않으면 TypeScript가 오류를 냄
}
}never 타입을 이용한 완전성 검사:
function assertNever(x: never): never {
throw new Error(`처리되지 않은 케이스: ${JSON.stringify(x)}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
return assertNever(shape); // 새 케이스 추가 시 여기서 컴파일 에러
}
}나중에 Shape에 'pentagon'을 추가하면 assertNever를 호출하는 줄에서 컴파일 에러가 납니다. 처리를 강제할 수 있습니다.
합 타입이 해결하는 문제
boolean 지옥
// boolean 플래그가 늘어나면 유효하지 않은 상태가 생김
type LoadingState = {
isLoading: boolean;
data: User[] | null;
error: string | null;
};
// 이런 상태가 이론상 가능:
// isLoading: true, data: [...], error: '오류' — 말이 안 됨합 타입으로 유효한 상태만 표현:
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
// 이제 'loading'이면서 data가 있는 상태는 불가능null 체크 지옥
// 합 타입 없이
function getUser(id: string): User | null {
// 호출하는 쪽에서 null 체크를 잊을 수 있음
}
// 합 타입으로 명시
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
function getUser(id: string): Result<User> {
// 호출하는 쪽이 ok를 확인하지 않으면 value에 접근 불가
}
const result = getUser('123');
if (result.ok) {
console.log(result.value.name); // 안전하게 접근
} else {
console.log(result.error);
}실용적인 ADT 예시
도메인 이벤트
type OrderEvent =
| { type: 'placed'; orderId: string; userId: string; items: Item[] }
| { type: 'paid'; orderId: string; amount: number }
| { type: 'shipped'; orderId: string; trackingNumber: string }
| { type: 'delivered'; orderId: string; deliveredAt: Date }
| { type: 'cancelled'; orderId: string; reason: string };
function handleOrderEvent(event: OrderEvent): void {
switch (event.type) {
case 'placed':
console.log(`주문 생성: ${event.orderId}, 사용자: ${event.userId}`);
break;
case 'paid':
console.log(`결제 완료: ${event.orderId}, 금액: ${event.amount}`);
break;
case 'shipped':
console.log(`배송 시작: ${event.trackingNumber}`);
break;
case 'delivered':
console.log(`배송 완료: ${event.deliveredAt}`);
break;
case 'cancelled':
console.log(`주문 취소: ${event.reason}`);
break;
}
}트리 구조 (재귀 ADT)
type Tree<T> =
| { kind: 'leaf'; value: T }
| { kind: 'node'; left: Tree<T>; right: Tree<T> };
function sum(tree: Tree<number>): number {
switch (tree.kind) {
case 'leaf':
return tree.value;
case 'node':
return sum(tree.left) + sum(tree.right);
}
}
const tree: Tree<number> = {
kind: 'node',
left: { kind: 'node', left: { kind: 'leaf', value: 1 }, right: { kind: 'leaf', value: 2 } },
right: { kind: 'leaf', value: 3 },
};
sum(tree); // 6정리
- 곱 타입: 모든 필드가 동시에 존재 —
A AND B(객체, 튜플) - 합 타입: 여러 경우 중 하나 —
A OR B(union type) - ADT: 두 조합을 사용해 복잡한 데이터 구조를 표현
합 타입은 불가능한 상태를 타입 시스템으로 막는 핵심 도구입니다. boolean 플래그를 조합하는 대신 합 타입으로 유효한 상태만 표현하면 런타임 오류를 컴파일 타임에 잡을 수 있습니다.