TypeScript 5.x 조건부 타입으로 API 응답 타입 추론 개선하기

문제 상황

프로젝트에서 REST API 클라이언트를 사용하는데, 엔드포인트마다 응답 타입을 일일이 제네릭으로 전달해야 했다.

const user = await api.get<UserResponse>('/users/123');
const posts = await api.get<PostListResponse>('/posts');

엔드포인트가 100개가 넘다 보니 타입을 잘못 지정하거나 누락하는 경우가 종종 발생했다.

해결 방법

조건부 타입과 템플릿 리터럴 타입을 조합해서 엔드포인트 경로로부터 응답 타입을 자동 추론하도록 개선했다.

type ApiEndpoints = {
  '/users/:id': UserResponse;
  '/posts': PostListResponse;
  '/posts/:id': PostDetailResponse;
};

type ExtractParams<T extends string> = 
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractParams<Rest>]: string }
    : T extends `${infer _Start}:${infer Param}`
    ? { [K in Param]: string }
    : {};

type ApiClient = {
  get<T extends keyof ApiEndpoints>(
    endpoint: T,
    params?: ExtractParams<T>
  ): Promise<ApiEndpoints[T]>;
};

이제 엔드포인트 경로만 입력하면 타입이 자동으로 추론된다.

const user = await api.get('/users/:id', { id: '123' }); // UserResponse
const posts = await api.get('/posts'); // PostListResponse

결과

  • 타입 캐스팅 에러가 80% 이상 감소했다
  • 새로운 엔드포인트 추가 시 ApiEndpoints만 확장하면 됨
  • IDE 자동완성으로 개발 속도도 개선되었다

다만 동적 라우팅이 복잡한 경우 타입 추론이 제대로 안 되는 경우가 있어서, 일부는 여전히 수동 타입 지정을 병행하고 있다.