함수 오버로딩
최근 라이브러리를 개발하는 과정에서 한 가지 타입 에러 때문에 꽤 오랫동안 골머리를 앓았다.
function foo<S, T extends Record<string, S>>(selector?: (state: T) => S) {
const { getBar } = useContext(bar);
if (selector) return selector(getBar())
else return getBar()
}
foo 함수는 selector라는 함수를 옵셔널하게 받고 있다. 이 selector 함수가 있으면 객체의 프로퍼티 일부만을 리턴하고, 없으면 객체 전체를 리턴하도록 만들었다. 나는 아규먼트 여부에 따라 타입스크립트가 그 리턴 값을 '정확하게' 추론해줄 거라 생각했다. 하지만 타입스크립트는 아규먼트 여부와 관계 없이 리턴 타입을 T | S로 추론했다.
내가 만들던 라이브러리는 전역 상태 관리 도구였기 때문에 리턴되는 값을 타입을 정확하게 추론해내는 게 중요했다. 따라서 처음에는 as const를 사용해보았다. 하지만 소용 없었다. 근본적으로 함수가 조건적으로 값을 리턴하고 있기 때문에 외부에서 해줄 수 있는 게 많지 않았다. 그러다가 함수 오버로딩이라는 방법을 알게 되었다.
오버로드 + 구현 시그니처
함수 오버로딩은 두 개의 시그니처로 구성된다. 하나의 함수에 대해 여러 버전의 타입을 정의하는 오버로드 시그니처와 실제 함수의 구현에 따른 타입을 정의하는 구현 시그니처가 그것이다.
위에서 내가 만든 함수는 selector 파라미터를 받느냐 받지 않느냐에 따라 서로 다른 버전이라고 생각할 수 있다. 오버로드 시그니처로는 이러한 버전들을 각각의 타입으로 작성하게 된다. 그리고 구현 시그니처는 ─ 실제 함수의 구현에 따른 타입을 정의하는 것이기 때문에 아주 당연하게도 ─ 위에서 작성한 코드를 그대로 사용하면 된다.
// selector를 받지 않는 버전에 대한 오버로드 시그니처
function foo<T extends Record<string, any>>(): T;
// selector를 받는 버전에 대한 오버로드 시그니처
function foo<S, T extends Record<string, S>>(selector: (state: T) => S): S;
// 실제 함수에 대한 구현 시그니처
function foo<S, T extends Record<string, S>>(selector?: (state: T) => S) {
const { getBar } = useContext(bar);
if (selector) return selector(getBar())
else return getBar()
}
오버로드 시그니처 제공 여부에 따라 타입 추론은 아래와 같이 달라지게 된다.
// 오버로드 시그니처가 제공되지 않은 foo 함수의 타입 추론
function foo<S, T extends Record<string, S>>(selector?: (state: T) => S): T | S;
// 오버로드 시그니처가 제공된 foo 함수의 타입 추론
function foo<T extends Record<string, any>>(): T (+1 overload);
블로그의 정보
Ayden's journal
Beard Weard Ayden