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 클라이언트 코드에도 적용 중이다. 조건부 타입은 생각보다 러닝커브가 낮으면서도 실용적이었다.

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