TypeScript 5.8 satisfies 연산자와 타입 추론 개선 사례

배경

레거시 코드베이스에서 설정 객체들의 타입 안정성이 부족했다. as const를 사용하면 너무 좁은 타입이 되고, 타입 어노테이션을 쓰면 리터럴 타입 정보가 손실되는 딜레마가 있었다.

TypeScript 5.8로 업그레이드하면서 satisfies 연산자의 개선된 타입 추론을 활용해 이 문제를 해결했다.

기존 방식의 문제

type RouteConfig = {
  path: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  handler: string;
};

// 방법 1: 타입 어노테이션 - 리터럴 타입 손실
const routes: Record<string, RouteConfig> = {
  userList: { path: '/users', method: 'GET', handler: 'getUsers' },
  userCreate: { path: '/users', method: 'POST', handler: 'createUser' }
};

// routes.userList.method는 string으로 추론됨

// 방법 2: as const - 타입 호환성 문제
const routes = {
  userList: { path: '/users', method: 'GET', handler: 'getUsers' }
} as const;

// 너무 좁은 타입으로 확장성 저하

satisfies 연산자 활용

const routes = {
  userList: { path: '/users', method: 'GET', handler: 'getUsers' },
  userCreate: { path: '/users', method: 'POST', handler: 'createUser' },
  userUpdate: { path: '/users/:id', method: 'PUT', handler: 'updateUser' }
} satisfies Record<string, RouteConfig>;

// 장점 1: 키 자동완성
const path = routes.userList.path; // OK

// 장점 2: 리터럴 타입 유지
type Method = typeof routes.userList.method; // 'GET'

// 장점 3: 타입 체크
const invalid = {
  test: { path: '/test', method: 'INVALID', handler: 'test' }
} satisfies Record<string, RouteConfig>; // 에러

실전 활용: 환경별 설정

type EnvConfig = {
  apiUrl: string;
  features: Record<string, boolean>;
  timeout: number;
};

const config = {
  development: {
    apiUrl: 'http://localhost:3000',
    features: { newUI: true, analytics: false },
    timeout: 5000
  },
  production: {
    apiUrl: 'https://api.example.com',
    features: { newUI: false, analytics: true },
    timeout: 3000
  }
} satisfies Record<string, EnvConfig>;

// 환경 키에 대한 타입 안정성
type Environment = keyof typeof config; // 'development' | 'production'

결론

satisfies 연산자 덕분에 타입 안정성과 추론의 균형을 맞출 수 있었다. 특히 설정 객체나 라우팅 테이블처럼 구조가 명확한 데이터에서 유용했다. 기존 코드베이스의 타입 어노테이션을 점진적으로 satisfies로 전환 중이다.