본문으로 건너뛰기
개발 머꼬
개발 노트JavaScript
hohyeon.dev18

TypeScript 유니온 타입에서 필드가 존재하는지 매번 옵셔널 체이닝한 이유

  • #Engineering Note
  • #Type Safety
  • #TypeScript

문제 발생

요청 상태(로딩 중/성공/실패)를 하나의 타입으로 표현했는데, 어디서든 dataerror에 접근하기 전에 항상 ?.(옵셔널 체이닝)과 null 체크를 반복해야 했습니다.

interface RequestState {
  status: "idle" | "loading" | "success" | "error";
  data?: User;
  error?: string;
}

function render(state: RequestState) {
  if (state.status === "success") {
    console.log(state.data.name); // 에러: data는 여전히 undefined일 수 있음
  }
}

원인 분석

위 인터페이스는 status"success"여도 TypeScript 입장에서는 dataUser | undefined라는 사실이 전혀 바뀌지 않습니다. statusdata/error가 서로 독립된 필드로 선언되어 있어서, 컴파일러는 "status"success"data가 반드시 존재한다"는 실제 비즈니스 규칙을 전혀 알지 못합니다. 그래서 매번 옵셔널 체이닝이나 non-null assertion(!)으로 우회해야 했던 것입니다.

해결 방안

**식별 유니온(discriminated union)**으로 상태별로 실제로 존재하는 필드가 다르다는 것을 타입 시스템에 직접 표현합니다. 공통 필드(status, "판별자"라고 부릅니다)의 값에 따라 나머지 필드 구성이 달라지는 여러 개의 타입을 유니온으로 묶습니다.

type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }   // success일 때만 data 존재
  | { status: "error"; error: string }; // error일 때만 error 존재

function render(state: RequestState) {
  if (state.status === "success") {
    console.log(state.data.name); // 에러 없음 — data가 확실히 존재함을 컴파일러가 앎
  }
}

if (state.status === "success")로 분기하는 순간, TypeScript는 자동으로 state의 타입을 { status: "success"; data: User }좁혀줍니다(narrowing) — 이제 data가 옵셔널이 아니라 반드시 존재하는 필드라는 것을 컴파일러가 보장합니다.

  1. "이 상태일 때만 이 필드가 존재한다"는 규칙이 있다면 옵셔널 필드로 어설프게 표현하지 말고 식별 유니온으로 명시적으로 나눕니다.
  2. switch (state.status)문과 함께 쓰면 모든 케이스를 다뤘는지 컴파일러가 검사해주는 것(exhaustiveness checking)도 활용할 수 있습니다 — default 분기에서 never 타입 체크를 넣어두면, 나중에 상태가 추가됐는데 분기 처리를 빠뜨렸을 때 컴파일 에러로 바로 알 수 있습니다.
  3. API 응답, 폼 상태, Redux 액션 타입처럼 "여러 변형이 있는 데이터"를 다룰 때 이 패턴이 특히 유용합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.