
아래와 같은 useMutataion 훅을 구현한다고 가정하자.
useMutataion 은 mutate와 isLoading을 return하고 mutate는 비동기적으로 opts.mutation을 호출하여 결과를 return한다.

useMutataion에서 인자로 받는 opts는 UseMutationOptions 타입이고

opts.mutation는 아래의 MutationBase 타입이다.

대력 아래와 같은 코드다.

이제 useMutataion을 사용하는데, 가상의 createUser라는 비동기 함수를 mutation으로 한다.


여기엔 사소한 문제점이 있는데, 아래와 같이 createUser에서 정한 타입에 대해 타입 체킹이 이루어지지 않고 있다는 것이다.


mutate의 인자 타입이 any[]로 지정되어 있기 때문이다.

궁극적으로 원하는 것은 useMutatation을 호출했을 때 createUser의 타입을 가져가는 것이다.


이제 useMutatation에 제대로된 타입을 지정해보자.
가장 먼저 useMutatation을 제네릭으로 변경을 한다.


그리고 UseMutationOptions에서 MutationBase를 TMutation으로 변경해준다.

제네릭으로 변경하게 되면 위와 같은 에러를 볼 수 있다. 타입을 제한해주지 않으면 unknown으로 인지하기 때문이다.
TMutation에 extends를 하여 제한을 걸어준다.

이제 에러도 사라졌다.

여전히 mutation.mutate의 타입은 MutationBase이다.

mutation.mutate의 타입도 제네릭하게 변경해줘야 한다.

UserMutationReturn에서 기존의 MutationBase를 TMutation으로 바꿔주면


제대로된 타입 체킹을 하는 것을 볼 수 있다.
하지만, 예상치 못한 에러를 발견하는데...
useMutation의 mutate에서 무언가 복잡한 컴파일 에러가 보인다.

ChatGPT의 도움을 받아 위 에러를 정리하자면 제네릭 타입 추론의 한계와 함수 시그니처 불일치가 원인이다.
TMutation은 (...args: any[]) => Promise<any>를 확장하지만 실제 구현체의 mutate는 (...args: any[]) => Promise<any>로 추론된다.
즉, 구현체의 mutate가 TMutation으로 자동 추론되지 않은 것이 문제다
TMutation이 더 구체적인 타입일 경우, (가령 위에서처럼 createUser) 타입 불일치 발생하는 것이다.
위와 같은 상황에서 타입 계층은 아래와 같다.
MutationBase (기저 타입, any[] 기반)
└── TMutation (구체적 타입, ex: createUser)
└── 구현체의 mutate 타입 (any[] 기반)
함수 매개변수는 역공변적으로 동작한다.
즉, 구현체의 매개변수 타입이 원본 타입의 상위 타입(supertype)이어야 한다. 그래서 반대 방향의 할당만 가능한 것이다.
(a: string) => ... → (...args: any[]) => ... 가능
(...args: any[]) => ... → (a: string) => ... 불가능
(함수 타입의 경우, 여러 개의 overloading과 계층 문제로 복잡해질 수도 있다고 한다.)
문제를 해결하기 위해서는 extends를 변경해주면 된다. MutationBase라고 함수 타입 전체를 퉁치는 것이 아니라 매개변수 타입과 반환 타입으로 나눠서 타입을 확장한다,

먼저, extends 대신 <TArgs, TReturn>으로 제네릭을 선언해준다.

그리고 TMutationBase도 <TArgs, TReturn>로 인자와 리턴 타입을 변경한다.

그리고 UseMutationReturn과 UseMutationOptions에도 동일하게 <TArgs, TReturn>를 적용해준다.

이제 구현체를 살펴보면 mutate도 제네릭으로 잘 추론되고

이제 구현체를 살펴보면 mutate의 인자 args는 TArgs로 잘 추론된다.

타입 파라미터 정밀화하기 위해서는 extends 함수형 타입 을 지양하는 것이 좋다.
아래는 전체 코드

참고 : total typescript