
이번에 TanStack Query에 Pull Request를 올렸고, Merge되었다. 좋은 경험이었기에 공유해보고자 한다.
https://github.com/TanStack/query/pull/8624
TanStack Query 레포지토리를 둘러보다 5개월 넘게 해결이 안 되고 있는 이슈를 발견했다.
https://github.com/TanStack/query/issues/7974
다음 코드에서 타입 에러가 발생한다는 제보였다.
const Queries1 = {
get: () =>
queryOptions({
queryKey: key1,
queryFn: async () => {
await sleep(10)
return 1
},
}),
}
const Queries2 = {
get: () =>
queryOptions({
queryKey: key2,
queryFn: async () => {
await sleep(10)
return true
},
}),
}
const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() }))
const result = useQueries({
queries: [...queries1List, { ...Queries2.get() }],
})

TanStack Query를 좋아하는 나로서는 기여할 수 있는 절호의 기회라 생각하여 곧바로 VS code를 켰다.
문제는 의외로 간단했다. useQueries의 선언은 다음과 같이 이루어진다.
export function useQueries<
T extends Array<any>,
TCombinedResult = QueriesResults<T>,
>(
{
queries,
...options
}: {
queries: readonly [...QueriesOptions<T>]
combine?: (result: QueriesResults<T>) => TCombinedResult
subscribed?: boolean
},
queryClient?: QueryClient,
): TCombinedResult {
// ...
}
여기서 queries의 readonly [...QueriesOptions<T>]가 […queries1List, { …Queries2.get() }]와 같은 동적 배열이 포함된 튜플 타입을 처리하지 못해 문제가 발생한 것이다.
이러한 동적 배열이 섞여있는 튜플 타입을 지원하기 위해 queries의 타입에 유니온으로 readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries<T[K]> }]를 추가해주었다.
queries:
| readonly [...QueriesOptions<T>]
| readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries<T[K]> }]
이렇게 하면 개별 요소의 타입을 유지할 수 있다.
끝난 줄 알았으나, 사용처에서 타입 추론에 문제가 발생하였다.
const data = result.map((item) => item.data)
에서 data의 타입이 unknown[]으로 추론되는 문제였다. 원래는 (number | boolean | undefined)[]로 추론되어야 정상이다.
이를 해결하기 위해서는 QueriesResults를 수정해야 했다.
원래 QueriesResults는 다음과 같다.
export type QueriesResults<
T extends Array<any>,
TResults extends Array<any> = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array<UseQueryResult>
: T extends []
? []
: T extends [infer Head]
? [...TResults, GetUseQueryResult<Head>]
: T extends [infer Head, ...infer Tails]
? QueriesResults<
[...Tails],
[...TResults, GetUseQueryResult<Head>],
[...TDepth, 1]
>
: T extends Array<
UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
any
>
>
? // Dynamic-size (homogenous) UseQueryOptions array: map directly to array of results
Array<
UseQueryResult<
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
>
: // Fallback
Array<UseQueryResult>
이번 문제도 간단히 해결되었다. 마지막 Fallback을 Array<GetUseQueryResult<T[number]>>로 수정하였더니 data가 깔끔히 추론되었고, 다음 테스트 코드가 통과했다.
expectTypeOf(result[0]?.data).toEqualTypeOf<
number | boolean | undefined
>()
그리고, 한 가지 문제를 더 발견했다.
result의 타입이 이상하게 추론되고 있었다.

[
...Array<UseQueryResult<number, Error>>,
UseQueryResult<boolean, Error>,
]
로 추론되기를 원하기에, 다음과 같이 테스트 코드를 작성했다.
expectTypeOf(result).toEqualTypeOf<
[
...Array<UseQueryResult<number, Error>>,
UseQueryResult<boolean, Error>,
]
>()
다시 QueriesResults를 수정했더니 result의 타입이 제대로 추론되었고, 테스트 코드가 통과되었다.
export type QueriesResults<
T extends Array<any>,
TResults extends Array<any> = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array<UseQueryResult>
: T extends []
? []
: T extends [infer Head]
? [...TResults, GetUseQueryResult<Head>]
: T extends [infer Head, ...infer Tails]
? QueriesResults<
[...Tails],
[...TResults, GetUseQueryResult<Head>],
[...TDepth, 1]
>
: {
[K in keyof T]: T[K] extends UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
any
>
? UseQueryResult<
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
: GetUseQueryResult<T[number]>
}
바뀐 부분을 비교하면 다음과 같다.
원래 코드
T extends Array<
UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
any
>
>
? Array<
UseQueryResult<
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
>
: Array<GetUseQueryResult<T[number]>>
1차 수정 코드
{
[K in keyof T]: T[K] extends UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
any
>
? UseQueryResult<
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
: GetUseQueryResult<T[number]>
}
T가 UseQueryOptionsForUseQueries의 형태를 띄는 서로다른 요소로 이루어진 배열일 경우에
T extends Array<
UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
any
>
>
가 false로 추론되기에 배열을 순회하면서 각 요소의 타입을 개별적으로 맵핑해주었고, 덕분에 각 인덱스별로 정확한 타입이 보존 가능해졌다.
이제 문제가 완전히 해결되었…는 줄 알았으나 동일한 방식을 solid-query의 createQueries에도 적용한 결과, 타입 테스트에서 에러가 발생했다.
이를 해결하기 위해 여러가지 시도를 해보았고, 마지막 GetUseQueryResult<T[number]>을 GetUseQueryResult<T[K]>로 바꿔 문제가 해결되었다. 이제 react-query, vue-query, svelte-query 뿐만 아니라 solid-query에서도 완벽히 작동한다!
그리고, 더 단순화할 수 없을까 생각해본 결과, 최종적으로 다음과 같이 코드가 완성되었다.
export type QueriesResults<
T extends Array<any>,
TResults extends Array<any> = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array<UseQueryResult>
: T extends []
? []
: T extends [infer Head]
? [...TResults, GetUseQueryResult<Head>]
: T extends [infer Head, ...infer Tails]
? QueriesResults<
[...Tails],
[...TResults, GetUseQueryResult<Head>],
[...TDepth, 1]
>
: { [K in keyof T]: GetUseQueryResult<T[K]> }
다시 바뀐 부분을 비교하면 다음과 같다.
1차 수정 코드
{
[K in keyof T]: T[K] extends UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
any
>
? UseQueryResult<
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
: GetUseQueryResult<T[number]>
}
2차 수정 코드
{
[K in keyof T]: T[K] extends UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
any
>
? UseQueryResult<
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
: GetUseQueryResult<T[K]>
}
3차 수정 코드
{ [K in keyof T]: GetUseQueryResult<T[K]> }
굳이 UseQueryOptionsForUseQueries를 extend하는지 비교하지 않고 직접 GetUseQueryResult<T[K]>를 사용하여 많이 간단해졌다.
이번 문제는 useQueries, useSuspenseQueries와 createQueries의 타입 추론이 튜플과 배열을 정확하게 처리하지 못하는 문제에서 시작되었다.
이를 해결하기 위해 다음과 같은 개선을 적용했다.
queries 타입 개선readonly [...QueriesOptions<T>]만으로는 동적 배열이 포함된 튜플을 처리할 수 없었음readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries<T[K]> }]을 추가하여 개별 요소의 타입을 유지할 수 있도록 변경QueriesResults 개선result 가 [...UseQueryResult<number, Error>[], UseQueryResult<boolean, Error>]처럼 개별 요소의 타입을 정확히 반영하도록 변경이번 개선을 통해 useQueries, useSuspenseQueries와 createQueries는 튜플과 배열을 정확하게 처리할 수 있는 타입 추론을 갖추게 되었으며,
각 요소의 타입이 유지되는 방식으로 변경되어 더 정밀한 타입 안전성을 제공할 수 있게 되었다.