TypeScript 4.6 템플릿 리터럴 타입으로 API 경로 타입 안전하게 관리하기

문제 상황

프로젝트에서 REST API 경로를 문자열로 하드코딩하다 보니 /api/users/:id 같은 동적 경로에서 오타가 자주 발생했다. 특히 리팩토링 시 경로가 변경되면 모든 호출부를 찾아 수정해야 했다.

// 기존 방식
const getUserUrl = (id: string) => `/api/users/${id}`;
const getPostUrl = (userId: string, postId: string) => 
  `/api/users/${userId}/posts/${postId}`;

템플릿 리터럴 타입 활용

TypeScript 4.1부터 지원된 템플릿 리터럴 타입을 활용해 타입 안전한 경로 빌더를 만들었다.

type ApiRoute = '/api/users' | '/api/posts';
type WithId<T extends string> = `${T}/${string}`;

type UserRoutes = '/api/users' | WithId<'/api/users'>;
type PostRoutes = WithId<'/api/users'> & `${string}/posts/${string}`;

function buildRoute<T extends string>(
  base: T,
  ...params: string[]
): `${T}/${string}` {
  return `${base}/${params.join('/')}` as `${T}/${string}`;
}

// 사용
const userUrl = buildRoute('/api/users', userId); // 타입 체크 통과
const wrongUrl = buildRoute('/api/wrong', id); // 컴파일 에러

실전 적용

API 클라이언트에 적용하니 잘못된 경로 사용이 IDE에서 즉시 감지됐다. 경로 변경 시에도 타입 에러로 모든 사용처를 찾을 수 있어 리팩토링이 안전해졌다.

class ApiClient {
  get<T>(route: UserRoutes | PostRoutes): Promise<T> {
    return fetch(route).then(r => r.json());
  }
}

const api = new ApiClient();
await api.get(`/api/users/${id}`); // OK
await api.get('/api/invalid'); // 컴파일 에러

타입 시스템으로 해결할 수 있는 영역이 계속 넓어지고 있다.