TypeScript 3.4 조건부 타입으로 API 응답 타입 안전하게 처리하기

문제 상황

사내 관리자 페이지를 TypeScript로 마이그레이션하던 중, API 응답 처리에서 타입 안전성을 확보하기 어려웠다. 기존 JavaScript 코드는 성공/실패를 런타임에서만 체크했고, 이를 그대로 타입으로 옮기니 매번 타입 가드를 작성해야 했다.

interface ApiResponse {
  success: boolean;
  data?: User[];
  error?: string;
}

위 구조는 successtrue일 때 data가 보장되지 않아, 사용처마다 옵셔널 체이닝과 타입 단언이 필요했다.

조건부 타입 적용

TypeScript 2.8부터 지원하는 조건부 타입으로 응답 타입을 재설계했다.

type ApiResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

function handleResponse<T>(response: ApiResponse<T>) {
  if (response.success) {
    // response.data는 T 타입으로 자동 추론
    return response.data;
  }
  // response.error는 string 타입으로 자동 추론
  throw new Error(response.error);
}

유니온 타입과 Discriminated Union을 조합하니 컴파일러가 분기를 정확히 인식했다.

제네릭 확장

여러 API 엔드포인트에 일관되게 적용하기 위해 유틸리티 타입을 추가했다.

type AsyncResult<T, E = Error> = 
  | { ok: true; value: T }
  | { ok: false; error: E };

async function fetchUsers(): Promise<AsyncResult<User[]>> {
  try {
    const res = await api.get('/users');
    return { ok: true, value: res.data };
  } catch (e) {
    return { ok: false, error: e.message };
  }
}

결과

타입 안전성이 확보되면서 런타임 에러가 줄었고, IDE 자동완성도 정확해졌다. 팀원들도 패턴을 금방 익혀서 다른 API 클라이언트 코드에도 적용 중이다. 조건부 타입은 생각보다 러닝커브가 낮으면서도 실용적이었다.