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

문제 상황

회사 프로젝트에서 API 경로를 문자열로 하드코딩하다 보니 오타로 인한 404 에러가 자주 발생했다. /api/users/${id}/api/user/${id}로 잘못 쓰는 경우가 대표적이었다.

// 기존 방식
const getUserUrl = (id: string) => `/api/users/${id}`;
const endpoint = getUserUrl('123'); // 오타 가능성

TypeScript 4.2 템플릿 리터럴 타입

지난 2월 말에 릴리즈된 TypeScript 4.2에서 템플릿 리터럴 타입이 추가되었다. 문자열 리터럴 타입을 조합해 새로운 타입을 만들 수 있다.

type APIResource = 'users' | 'posts' | 'comments';
type APIVersion = 'v1' | 'v2';
type APIPath = `/api/${APIVersion}/${APIResource}`;

// 자동완성되는 경로들
// '/api/v1/users' | '/api/v1/posts' | ... (총 6개 조합)

실제 적용

프로젝트에 적용해본 패턴이다.

type RESTMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = `/api/users` | `/api/users/${string}` | `/api/posts`;

interface APIRequest {
  method: RESTMethod;
  endpoint: Endpoint;
}

const fetchAPI = ({ method, endpoint }: APIRequest) => {
  return fetch(endpoint, { method });
};

// 타입 안전한 호출
fetchAPI({ method: 'GET', endpoint: '/api/users' }); // OK
fetchAPI({ method: 'GET', endpoint: '/api/user' }); // 컴파일 에러

한계와 대안

동적 경로 파라미터 처리는 여전히 까다롭다. /api/users/${string}으로 표현할 수 있지만 타입 추론이 약하다. 실무에서는 주요 엔드포인트만 리터럴로 정의하고, 동적 부분은 헬퍼 함수로 분리했다.

const createUserEndpoint = (id: string): `/api/users/${string}` => {
  return `/api/users/${id}`;
};

팀 내 리뷰에서는 호평이었다. 단순하지만 실수를 줄이는 데 확실히 효과가 있었다. 다음 스프린트부터 모든 API 클라이언트 코드에 적용하기로 했다.

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