기본 패턴
ts
type Loading = { state: "loading" };
type Success = { state: "success"; data: string[] };
type Failure = { state: "failure"; message: string };
type Result = Loading | Success | Failure;
function render(result: Result) {
switch (result.state) {
case "loading":
return "loading...";
case "success":
return result.data.join(", ");
case "failure":
return result.message;
default: {
const _exhaustive: never = result;
return _exhaustive;
}
}
}설명
- discriminated union은 여러 객체 타입이 공통 태그 필드 하나를 공유하도록 설계한 union입니다.
state,kind,type같은 필드가 분기 기준이 됩니다. - 장점은 TypeScript가 분기문 안에서 태그 값을 보고 자동으로 타입을 좁힐 수 있다는 점입니다. 그래서
if와switch가 단순 제어 흐름이 아니라 타입 안전한 모델링 도구가 됩니다. - 이 패턴은 네트워크 상태, 폼 상태, 비동기 결과, reducer action처럼 "서로 다른 경우의 수"가 분명한 데이터를 표현할 때 특히 강합니다.
never를 이용한 exhaustiveness check는 새 케이스를 나중에 추가했을 때 기존 분기문이 빠짐없이 수정되도록 도와줍니다. 즉 런타임 버그를 컴파일 단계로 끌어올리는 장치입니다.- union이 커질수록 중요한 것은 문법보다 도메인 모델링입니다. 태그 이름을 일관되게 유지하고, 각 분기가 가져야 할 데이터만 분명하게 넣는 설계가 더 중요합니다.
빠른 정리
| 요소 | 역할 |
|---|---|
| 태그 필드 | 분기 기준 |
| union | 가능한 상태 집합 |
| narrowing | 분기 안에서 타입 자동 축소 |
never | 빠진 분기를 컴파일러가 잡게 함 |
| 잘 맞는 곳 | async 상태, action, 화면 상태 |
주의할 점
태그 필드 없이 union만 나열하면 분기할 때마다 수동 타입 단언이 늘어나기 쉽습니다. 분기가 중요한 데이터라면 처음부터 discriminated union으로 설계하는 편이 훨씬 안정적입니다.
참고 링크
2 sources