타입스크립트 Union 타입 좁히기 패턴

문제 상황

레거시 API가 성공/실패 여부에 따라 다른 응답 구조를 반환하는데, 이를 처리하는 코드가 타입 체크를 제대로 받지 못하고 있었다.

interface SuccessResponse {
  data: User[];
  total: number;
}

interface ErrorResponse {
  error: string;
  code: number;
}

type ApiResponse = SuccessResponse | ErrorResponse;

문제는 response.data에 접근할 때마다 타입스크립트가 에러를 냈고, 결국 as any로 도배하게 됐다는 점이다.

Discriminated Union 적용

공통 속성을 추가해 타입을 구분할 수 있도록 수정했다.

interface SuccessResponse {
  success: true;
  data: User[];
  total: number;
}

interface ErrorResponse {
  success: false;
  error: string;
  code: number;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.success) {
    // 여기서 response는 자동으로 SuccessResponse 타입
    console.log(response.data.length);
  } else {
    // 여기서는 ErrorResponse 타입
    console.error(response.error);
  }
}

success 필드 하나로 타입 가드가 자동으로 작동한다. 조건문 내부에서 타입스크립트가 타입을 정확히 추론해준다.

Type Predicate 함수

반복적으로 사용되는 타입 가드는 별도 함수로 분리했다.

function isSuccess(response: ApiResponse): response is SuccessResponse {
  return response.success === true;
}

if (isSuccess(response)) {
  // SuccessResponse로 좁혀짐
  return response.data;
}

is 키워드를 사용한 type predicate 덕분에 함수 호출만으로 타입 좁히기가 가능해졌다.

결과

  • any 사용 12곳 제거
  • 런타임 에러 가능성 있던 부분 컴파일 타임에 감지
  • 코드 가독성 개선

Union 타입은 공통 속성(discriminant)만 잘 설계하면 타입 안정성을 크게 높일 수 있다.

타입스크립트 Union 타입 좁히기 패턴