Overloaded function interface의 전체 파라미터 타입 추론

TypeScript에서는 Parameter<T>를 사용해서 overloaded function interface의 전체 파라미터 타입을 추론하는 것에 한계가 있다. infer 키워드를 사용해서 오버로딩된 함수들의 파라미터 타입을 추론하고, 그것을 바탕으로 union 타입을 선언할 수 있다.

TypeScript에서는 Parameter<T>를 사용해서 overloaded function interface의 전체 파라미터 타입을 추론하는 것에 한계가 있다. react-router에 선언되어 있는 NavigateFunction의 파라미터 타입을 추론하고자 삽질하며 얻은 경험을 기록하고자 한다.

Motivation

// Given function interface
export interface NavigateFunction {
  (to: To, options?: {
    replace?: boolean;
    state?: State;
  }): void;
  (delta: number): void;
}

// Expected params type
type ExpectedParamsType =
  | [to: To, options?: { replace?: boolean; state?: State; }]
  | [delta: number]
react-router 모듈의 타입 선언 중 일부

위 예시로 가져온 NavigateFunction의 함수 파라미터를 ExpectedParamsType 타입으로 추론하려면 어떻게 해야 할까? 가장 직관적으로 떠오르는 방법은 TypeScript에서 기본으로 제공되는  유틸리티 타입Parameters<T>를 사용하는 것일지도 모르겠다.

Parameters<NavigateFunction>의 타입

하지만 Parameters<T>를 사용해보면 실제로 추론된 파라미터 튜플은 가장 마지막으로 오버로딩된 함수의 파라미터만을 포함하고 있음을 확인할 수 있다.

검색해보니, TypeScript 레포지토리에 위 ExpectedParamsType과 같이 파라미터 타입 추론이 이루어져야 한다는 issue가 개설된 바 있음을 확인하였다.

Parameter type interface for overloaded functions as union type · Issue #32164 · microsoft/TypeScript
Search Terms parameter type interface for overloaded functions as union type Suggestion The following method: /** * Obtain the parameters of a function type in a tuple */ type Parameters&lt;T exten...

(나만 이상하다고 생각한게 아니었구나! 😂)

TypeScript 메인테이너의 설명과 예제를 읽어보면, 오버로딩된 함수의 모든 파라미터 타입을 union으로 반환하는 것은 Parameters<T>[0] 처럼 반환된 union 튜플의 특정 인덱스를 참조할 때 전체적인 타입 정의를 애매모호하게(저자의 표현을 빌리면 "useful and sound"하지 않게끔) 만들 수 있기 때문에 그렇게 구현하지 않았다는 요지인 것으로 들린다.

Inferring Parameters Type of Overloaded Function

개인적으로는 설득력있는 예제인 것 같지는 않지만, 어쨌든 메인테이너가 싫다는데 별 수가 없다. 내가 사용한 방법은 infer 키워드를 사용해서 오버로딩된 함수들의 파라미터 타입을 추론하고, 그것을 바탕으로 union 타입을 선언하는 방식이다.

// Given function interface
export interface NavigateFunction {
  (to: To, options?: {
    replace?: boolean;
    state?: State;
  }): void;
  (delta: number): void;
}

// Union type using infer keyword
type NavigateFunctionParams = NavigateFunction extends {
  (...args: infer A1): any;
  (...args: infer A2): any;
} ? A1 | A2
  : never;

이렇게 하면 NavigateFunction의 오버로딩된 함수들의 파라미터를 union 타입으로 불러올 수 있다.

infer 키워드를 사용해 추론한 파라미터 타입

물론 이 방법도 명확한 한계점은 있는데, 바로 오버로딩된 함수의 갯수가 하드코딩되어 있다는 점이다. 즉, NavigateFunction 인터페이스에 새로운 함수 타입이 추가될 경우 infer A3 키워드를 추가하고, A1 | A2 | A3와 같이 타입 선언을 수정해줄 필요가 있다. 위에 북마크한 issue를 읽어보면, 전 세계의 TypeScript 고수들이 내놓은 다양(하고 기괴)한 방법을 구경할 수 있다.

Conclusion?

이렇게 고생스럽게 오버로딩된 함수의 파라미터 타입을 정의하였다. 하지만 잘 정의된 파라미터 타입을 가져다 쓰는 것조차 쉽지 않은 일이다. 왜냐하면 TypeScript는 오버로딩된 함수의 호출 시그니처를 제대로 처리하지 못하기 때문이다. 물론 "쉽지 않은 일"이라고 하기에는 type assertion을 통해 간단히 해결하는 방법이 있긴 하지만, TypeScript를 쓰는 입장에서 아쉬움이 남는 것도 사실이다.