실무에서 상태값이나 응답 타입, 이벤트 타입을 처리하다 보면 “이 값이 어떤 형태인지”에 따라 분기해야 하는 경우가 매우 많습니다. 이때 단순한 if / else 분기만으로는 타입 안정성을 완전히 보장하기 어렵습니다.
TypeScript에서는 이러한 문제를 해결하기 위해 Discriminated Union(식별 가능한 유니온) 패턴을 제공합니다. 이 패턴을 사용하면 컴파일 단계에서 분기 누락과 잘못된 접근을 모두 차단할 수 있습니다.
Discriminated Union이란?
Discriminated Union은 다음 두 가지 조건을 만족하는 유니온 타입입니다.
- 모든 타입이 공통된 식별자 필드를 가진다
- 그 필드는 리터럴 타입이다
type Loading = {
status: "loading";
};
type Success = {
status: "success";
data: string;
};
type Failure = {
status: "error";
message: string;
};
type Result = Loading | Success | Failure;
여기서 status가 바로 discriminant(식별자) 역할을 합니다.
안전한 분기 처리의 핵심
Discriminated Union의 가장 큰 장점은 조건 분기 안에서 타입이 자동으로 좁혀진다는 점입니다.
function handleResult(result: Result) {
if (result.status === "success") {
return result.data; // Success 타입으로 자동 추론
}
if (result.status === "error") {
return result.message; // Failure 타입
}
return "로딩 중...";
}
별도의 타입 단언(as) 없이도 안전하게 필드 접근이 가능합니다.
switch + never 패턴 (실무 필수)
실무에서는 switch 문과 never를 함께 사용하는 패턴이 가장 권장됩니다.
function handle(result: Result): string {
switch (result.status) {
case "loading":
return "로딩 중...";
case "success":
return result.data;
case "error":
return result.message;
default:
const _exhaustive: never = result;
return _exhaustive;
}
}
이 방식의 핵심 장점은 다음과 같습니다.
- 새로운 상태가 추가되면 컴파일 에러 발생
- 분기 누락을 코드 리뷰 이전에 차단
- 타입 안정성과 유지보수성 모두 확보
대규모 코드베이스에서는 이 패턴이 사실상 표준입니다.
실전 예제 — 도형 계산
Discriminated Union은 수학적 모델링, 도메인 모델링에서도 자주 활용됩니다.
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
size: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Shape = Circle | Square | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
default:
const _never: never = shape;
return _never;
}
}
각 분기에서 정확한 타입만 접근 가능하므로 계산 로직이 매우 안전해집니다.
API 응답 모델링에서의 활용
백엔드 API 응답을 모델링할 때 Discriminated Union은 특히 강력합니다.
type ApiResponse =
| { ok: true; data: { id: number; name: string } }
| { ok: false; errorCode: string };
function handleApi(res: ApiResponse) {
if (res.ok) {
return res.data.name;
}
return `에러 코드: ${res.errorCode}`;
}
ok 값 하나로 성공/실패 분기가 완전히 보장됩니다.
Discriminated Union을 써야 하는 이유
- 타입 단언(as) 제거
- null / undefined 체크 감소
- 상태 추가 시 컴파일 에러로 즉시 감지
- 도메인 모델이 명확해짐
- 리팩토링 안정성 극대화
특히 상태가 늘어날 가능성이 있는 로직에서는 Discriminated Union이 사실상 유일한 정답에 가깝습니다.
실무 적용 시 주의사항
- 식별자 필드는 반드시 리터럴 타입으로 선언
- optional 식별자는 피할 것
- 가능하면 switch + never 패턴 사용
- if/else 중첩보다 가독성 높은 구조 유지
이 원칙만 지켜도 분기 관련 버그는 눈에 띄게 줄어듭니다.
Discriminated Union 패턴은 “타입으로 분기를 강제하는 설계 기법”입니다. 런타임에 발생할 수 있는 분기 오류를 컴파일 단계에서 차단함으로써 안정적이고 확장 가능한 코드를 만들 수 있습니다.
- 공통 식별자 + 리터럴 타입
- switch + never로 완전성 보장
- 상태/이벤트/API 모델링에 최적
'개발 > Typescript' 카테고리의 다른 글
| [TYPESCRIPT] Index Signature와 동적 객체 타입 - 키가 정해지지 않은 객체를 안전하게 다루는 방법 (0) | 2025.12.20 |
|---|---|
| [TYPESCRIPT] Readonly와 const assertion - 불변성을 타입으로 보장하는 방법 (0) | 2025.12.19 |
| [TYPESCRIPT] 조건부 타입(Conditional Type) — 타입에 따라 분기하는 고급 타입 설계 (0) | 2025.12.15 |
| [TYPESCRIPT] infer 키워드로 타입 추론하기 — 조건부 타입의 핵심 메커니즘 (0) | 2025.12.14 |
| [TYPESCRIPT] Record, Exclude, Extract, NonNullable 정리 — 실무에서 자주 쓰는 타입 변환 도구 (0) | 2025.12.13 |
