이 글은 Chat GPT로 TypeScript를 공부하며 정리한 글입니다.
TypeScript의 유니온 타입(Union Type) 은 여러 타입 중 하나를 가질 수 있는 타입이다.
이때 Discriminated Union(차별화된 유니온 타입) 을 사용하면,
모든 타입에 공통된 식별 필드(discriminant) 를 두고
그 값의 literal 타입(예: "loading" | "success" | "error")을 기준으로 안전하게 타입을 좁힐 수 있다.
그러나 유니온 내의 각 타입이 서로 완전히 다른 구조를 가지면
“현재 어떤 타입이 선택됐는지”를 좁히기(narrowing) 어려울 수 있다.
비동기 요청의 상태를 타입으로 표현해보자.
type FetchState<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
interface Post {
id: number;
title: string;
body: string;
}
type PostState = FetchState<Post[]>;
FetchState<T>는 다음 세 가지 상태를 표현한다
| 상태 | 설명 |
|---|---|
"loading" | 데이터를 불러오는 중 |
"success" | 요청 성공, data 보유 |
"error" | 요청 실패, error 메시지 보유 |
여기서 <T>는 제네릭으로, 상태에 담길 데이터 타입을 외부에서 지정할 수 있다.
아래처럼 status 기반 조건문을 작성하면,
TypeScript는 control-flow analysis로 자동 narrowing 을 수행한다.
function render(state: FetchState<Post[]>) {
if (state.status === "loading") {
return "로딩중...";
}
if (state.status === "error") {
return `에러 발생: ${state.error}`;
}
if (state.status === "success") {
return state.data.map(p => p.title).join(", ");
}
}
TypeScript는 각 분기에서 다음과 같이 타입을 자동 추론한다.
| status 값 | 자동 추론 타입 |
|---|---|
"loading" | { status: "loading" } |
"error" | { status: "error"; error: string } |
"success" | { status: "success"; data: T } |
이 구조 덕분에 error는 "error" 상태에서만, data는 "success" 상태에서만 접근 가능해진다.
즉, 타입 안정성(type safety) 이 확실하게 보장된다.
| 개념 | 설명 |
|---|---|
| Discriminated Union | 공통된 literal 필드(status)로 타입을 구분 |
| Type Narrowing | 조건문을 통해 타입을 안전하게 좁히는 과정 |
| 장점 | 타입 안전성, 명확한 로직, 런타임 에러 감소 |
비동기 상태 외에도 다음 상황에서 자주 활용된다.
type ValidationResult =
| { valid: true; value: string }
| { valid: false; message: string };
function validate(input: string): ValidationResult {
if (input.trim() === "") {
return { valid: false, message: "값이 비어 있습니다." };
}
return { valid: true, value: input };
}
여기서 valid 필드가 구분자(discriminant) 역할을 한다.
프론트엔드 개발에서는 비동기 요청 상태가 자주 등장한다.
loading)success)error)이를 Discriminated Union으로 표현하면
React 컴포넌트의 상태 관리가 훨씬 더 안전하고 명확해진다.
type FetchState<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
interface Post {
id: number;
title: string;
body: string;
}
import { useEffect, useState } from "react";
export default function PostList() {
const [state, setState] = useState<FetchState<Post[]>>({ status: "loading" });
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts")
.then(res => {
if (!res.ok) throw new Error("요청 실패");
return res.json();
})
.then(data => setState({ status: "success", data }))
.catch(err => setState({ status: "error", error: err.message }));
}, []);
if (state.status === "loading") {
return <p>로딩 중...</p>;
}
if (state.status === "error") {
return <p>에러 발생: {state.error}</p>;
}
if (state.status === "success") {
return (
<ul>
{state.data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
return null; // 현재 유니온 구조에서는 사실상 도달되지 않음
}
| 장점 | 설명 |
|---|---|
| 타입 안정성 | 상태에 따라 TypeScript가 타입을 정확하게 좁힘 |
| 오타 미검출 방지 | "sucess" 같은 오타는 literal type 덕에 즉시 오류 |
| 명확한 로직 | 로딩/성공/실패 상태가 구조적으로 분리됨 |
| 확장성 | "idle", "refetching" 등 상태 추가가 쉬움 |
Discriminated Union은
비동기 상태 관리, 폼 검증, 서버 응답 처리 등
“조건 분기 기반 로직”을 타입 안전하게 만들 때 가장 강력한 기법 중 하나다.
React 뿐 아니라 Zustand, Redux Toolkit, TanStack Query 같은 상태 관리 라이브러리에서도
상태 객체를 구조적으로 설계할 때 매우 유용하다.