TypeScript 5.4의 NoInfer 유틸리티 타입 활용기

문제 상황

회사 프로젝트에서 옵션 객체를 받는 제네릭 함수를 만들다가 타입 추론이 의도와 다르게 동작하는 문제를 겪었다.

function createConfig<T extends string>(options: {
  mode: T;
  fallback: T;
}) {
  return options;
}

const config = createConfig({
  mode: 'light',
  fallback: 'dark'
});
// T는 'light' | 'dark'로 추론됨 (원하는 것: 'light')

mode를 기준으로 타입을 좁히고 싶었지만, TypeScript는 두 프로퍼티 모두에서 타입을 추론해 유니온으로 만들어버렸다.

NoInfer 적용

TypeScript 5.4에서 추가된 NoInfer<T> 유틸리티 타입을 사용하면 특정 위치에서 타입 추론을 제외할 수 있다.

function createConfig<T extends string>(options: {
  mode: T;
  fallback: NoInfer<T>;
}) {
  return options;
}

const config = createConfig({
  mode: 'light',
  fallback: 'dark' // 에러: 'dark'는 'light' 타입에 할당 불가
});

fallback을 NoInfer<T>로 감싸자 mode에서만 타입이 추론되고, fallback은 그 타입을 따르게 되었다.

실전 활용 사례

프로젝트의 이벤트 핸들러 등록 함수에 적용했다.

type EventMap = {
  click: { x: number; y: number };
  submit: { formData: FormData };
};

function addEventListener<K extends keyof EventMap>(
  event: K,
  handler: (data: NoInfer<EventMap[K]>) => void
) {
  // 구현
}

addEventListener('click', (data) => {
  console.log(data.x, data.y); // 타입 안전
});

handler의 파라미터에서 타입이 역추론되는 것을 방지하여 event 문자열 기반으로만 타입이 결정되도록 했다.

결론

NoInfer는 제네릭 함수에서 타입 추론 방향을 명확히 제어할 수 있게 해준다. 특히 여러 곳에서 같은 제네릭 타입을 사용할 때 의도치 않은 타입 확장을 방지하는 데 유용했다. 5.4 업그레이드 후 기존 타입 헬퍼 함수 몇 개를 리팩토링할 수 있었다.