TypeScript 4.7 template literal type으로 API 라우트 타입 안전하게 관리하기

문제 상황

프로젝트에서 API 엔드포인트를 호출할 때 경로를 하드코딩하다 보니 오타로 인한 런타임 에러가 자주 발생했다. /api/users/:id 같은 동적 경로의 경우 파라미터 타입도 보장되지 않았다.

TypeScript 4.7 template literal type 개선

4.7 버전에서 template literal type의 타입 추론이 크게 개선되었다. 이를 활용해 API 라우트를 타입 안전하게 만들 수 있었다.

type ApiRoute = 
  | '/api/users'
  | `/api/users/${string}`
  | '/api/posts'
  | `/api/posts/${string}`;

type ExtractParams<T extends string> = 
  T extends `${string}/${infer Param}` 
    ? Param extends `${infer P}/${infer Rest}`
      ? P | ExtractParams<Rest>
      : Param
    : never;

function apiCall<T extends ApiRoute>(
  route: T,
  params: ExtractParams<T> extends string ? { id: string } : void
) {
  // 실제 API 호출 로직
}

// 타입 안전한 호출
apiCall('/api/users/123', { id: '123' }); // OK
apiCall('/api/users'); // OK
apiCall('/api/users/123'); // params 필요 - 에러

실전 적용

프로젝트에서는 좀 더 실용적으로 접근했다.

const API_ROUTES = {
  users: {
    list: '/api/users',
    detail: (id: string) => `/api/users/${id}` as const,
  },
  posts: {
    list: '/api/posts',
    detail: (id: string) => `/api/posts/${id}` as const,
  },
} as const;

type ApiClient = {
  get<T>(url: string): Promise<T>;
};

const client: ApiClient = axios.create();

// 사용
const userId = '123';
const user = await client.get(API_ROUTES.users.detail(userId));

함수로 감싸면서 파라미터 타입을 강제할 수 있고, as const로 리터럴 타입을 유지했다.

결과

  • API 경로 오타로 인한 런타임 에러 제거
  • 파라미터 타입 자동 검증
  • IDE 자동완성 지원

TypeScript 4.7의 template literal type 개선으로 이전보다 훨씬 정교한 타입 시스템을 구축할 수 있게 되었다.