숏컷 코드
type Result =
| { kind: "ok"; value: string }
| { kind: "error"; message: string };
function render(result: Result) {
switch (result.kind) {
case "ok":
return result.value;
case "error":
return result.message;
}
}태그 분기
discriminated union에서 먼저 보는 기본형은 아래입니다.
- 공통 태그 필드가 있는 union
- 태그 값이 리터럴로 고정된 멤버
switch (state.kind)분기assertNever를 통한 exhaustiveness 체크- optional 필드 묶음 대신 상태별 구조 분리
1) 공통 태그 필드로 상태를 나누면 discriminated union
모든 멤버가 kind, status, type 같은 공통 리터럴 필드를 공유하면 TypeScript가 그 값을 기준으로 안전하게 narrowing할 수 있습니다.
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: Error };상태 모델링과 API 결과 표현에서 가장 자주 쓰이는 패턴 중 하나입니다.
2) switch 분기와 붙이면 각 상태 전용 필드를 안전하게 쓴다
공통 태그를 기준으로 분기하면, 각 케이스 안에서 그 멤버 전용 필드를 바로 쓸 수 있습니다.
function render(state: State) {
switch (state.status) {
case "success":
return state.data.length;
case "error":
return state.error.message;
}
}핵심은 state.status가 단순 string이 아니라 각 멤버의 리터럴 값으로 유지돼야 한다는 점입니다.
3) exhaustiveness 체크는 새 케이스 누락을 잡는다
never를 이용한 exhaustive check를 넣으면 union에 새 멤버가 추가됐는데 분기를 갱신하지 않은 경우 컴파일 단계에서 드러낼 수 있습니다.
function assertNever(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}function render(state: State) {
switch (state.status) {
case "idle":
return "idle";
case "loading":
return "loading";
case "success":
return state.data.length;
case "error":
return state.error.message;
default:
return assertNever(state);
}
}4) 태그 값이 넓어지면 분기 이점이 약해진다
discriminated union이 잘 작동하려면 태그 필드가 string 같은 넓은 타입이 아니라 "idle" | "loading" 같은 리터럴이어야 합니다.
// ❌ 태그가 너무 넓다
type BadState = {
status: string;
data?: string[];
};
// ✅ 태그가 케이스를 직접 나타낸다
type GoodState =
| { status: "idle" }
| { status: "success"; data: string[] };5) 느슨한 optional 조합보다 상태별 구조 분리가 낫다
loading?: boolean, data?: ..., error?: ... 같은 느슨한 객체 하나에 모든 상태를 몰아넣으면 어떤 조합이 유효한지 흐려집니다.
상태가 실제로 다르다면 union으로 분리하는 편이 더 강합니다.
6) 상태 수가 늘어날수록 이 패턴의 가치가 커진다
작은 컴포넌트보다, 상태와 케이스가 계속 늘어나는 프로젝트에서 discriminated union과 exhaustiveness 체크의 체감 이득이 커집니다.
체크 기준
- 상태/결과를 몇 가지 리터럴 케이스로 구분한다: discriminated union
- 공통 태그 필드를 쓴다:
kind,status,type - 태그 값이 리터럴로 유지된다: discriminated union이 잘 맞음
- 분기별 전용 필드를 안전하게 쓴다: switch/if narrowing
- 새 케이스 누락을 막는다:
neverexhaustiveness check - optional 필드를 한 객체에 몰아넣고 있다: union 분리 검토
주의할 점
옵셔널 필드 몇 개를 한 객체에 몰아넣는 방식은 상태를 느슨하게 만들기 쉽습니다. 정말 서로 다른 상태라면 discriminated union이 더 낫습니다.
// ❌ 어떤 조합이 유효한지 모호함
interface FetchState {
loading?: boolean;
data?: string[];
error?: Error;
}
// ✅ 상태별 구조를 분리
type FetchState =
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: Error };참고 링크
1 sources