TypeScript 4.9의 satisfies 연산자로 타입 안정성 높이기

배경

프로젝트에서 라우트 설정 객체를 관리하는데, 기존 방식은 타입 안정성과 추론 사이에서 trade-off가 있었다. as const를 쓰면 추론은 좋지만 타입 체크가 약하고, 타입 어노테이션을 명시하면 리터럴 타입이 넓어지는 문제가 있었다.

기존 방식의 문제

type Route = {
  path: string;
  methods: string[];
};

// 방법 1: 타입 체크는 되지만 추론이 약함
const routes: Route = {
  path: '/api/users',
  methods: ['GET', 'POST'] // string[]로 추론됨
};

// 방법 2: 추론은 좋지만 오타 체크 안됨
const routes = {
  path: '/api/users',
  methds: ['GET', 'POST'] // 오타인데 에러 없음
} as const;

satisfies 연산자

TypeScript 4.9에서 추가된 satisfies 연산자로 두 마리 토끼를 잡았다.

const routes = {
  path: '/api/users',
  methods: ['GET', 'POST']
} as const satisfies Route;

// 장점 1: methods가 readonly ['GET', 'POST']로 좁게 추론
type Methods = typeof routes.methods[number]; // 'GET' | 'POST'

// 장점 2: 오타나 잘못된 필드는 컴파일 에러
const invalid = {
  ppath: '/api/users', // Error: Object literal may only specify known properties
  methods: ['GET']
} as const satisfies Route;

실무 적용

API 엔드포인트 설정에 적용하니 자동완성이 정확해지고, 런타임 오류가 줄었다.

const API_ROUTES = {
  users: { path: '/api/users', methods: ['GET', 'POST'] },
  posts: { path: '/api/posts', methods: ['GET', 'PUT', 'DELETE'] }
} as const satisfies Record<string, Route>;

// 타입 안전하게 사용 가능
function request(route: typeof API_ROUTES[keyof typeof API_ROUTES]) {
  // route.methods는 정확한 리터럴 타입
}

결론

satisfies는 타입 체크와 타입 추론을 모두 살릴 수 있는 좋은 도구다. 특히 설정 객체나 상수 정의에 유용했다. TypeScript 4.9 이상이라면 적극 활용할 만하다.