TypeScript 3.4 조건부 타입으로 API 응답 타입 안전하게 처리하기
문제 상황
사내 관리자 페이지를 TypeScript로 마이그레이션하던 중, API 응답 처리에서 타입 안전성을 확보하기 어려웠다. 기존 JavaScript 코드는 성공/실패를 런타임에서만 체크했고, 이를 그대로 타입으로 옮기니 매번 타입 가드를 작성해야 했다.
interface ApiResponse {
success: boolean;
data?: User[];
error?: string;
}
위 구조는 success가 true일 때 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 클라이언트 코드에도 적용 중이다. 조건부 타입은 생각보다 러닝커브가 낮으면서도 실용적이었다.