Skip to content

Commit

Permalink
fix(vue-query): use distributive omit to preserve union (#4562)
Browse files Browse the repository at this point in the history
  • Loading branch information
henribru committed Nov 28, 2022
1 parent 70fd5bb commit f6d92a7
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 6 deletions.
10 changes: 10 additions & 0 deletions packages/vue-query/src/__tests__/test-utils.ts
Expand Up @@ -50,3 +50,13 @@ export function successMutator<T>(param: T): Promise<T> {
export function errorMutator<T>(_: T): Promise<Error> {
return rejectFetcher()
}

export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
T,
>() => T extends Y ? 1 : 2
? true
: false

export type Expect<T extends true> = T

export const doNotExecute = (_func: () => void) => true
82 changes: 82 additions & 0 deletions packages/vue-query/src/__tests__/useInfiniteQuery.types.test.tsx
@@ -0,0 +1,82 @@
import { InfiniteData } from '@tanstack/query-core'
import { reactive } from 'vue'
import { useInfiniteQuery } from '../useInfiniteQuery'
import { doNotExecute, Equal, Expect, simpleFetcher } from './test-utils'

describe('Discriminated union return type', () => {
it('data should be possibly undefined by default', () => {
doNotExecute(() => {
const query = reactive(
useInfiniteQuery({
queryFn: simpleFetcher,
}),
)

const result: Expect<
Equal<InfiniteData<string> | undefined, typeof query.data>
> = true
return result
})
})

it('data should be defined when query is success', () => {
doNotExecute(() => {
const query = reactive(
useInfiniteQuery({
queryFn: simpleFetcher,
}),
)

if (query.isSuccess) {
const result: Expect<Equal<InfiniteData<string>, typeof query.data>> =
true
return result
}
})
})

it('error should be null when query is success', () => {
doNotExecute(() => {
const query = reactive(
useInfiniteQuery({
queryFn: simpleFetcher,
}),
)

if (query.isSuccess) {
const result: Expect<Equal<null, typeof query.error>> = true
return result
}
})
})

it('data should be undefined when query is loading', () => {
doNotExecute(() => {
const query = reactive(
useInfiniteQuery({
queryFn: simpleFetcher,
}),
)

if (query.isLoading) {
const result: Expect<Equal<undefined, typeof query.data>> = true
return result
}
})
})

it('error should be defined when query is error', () => {
doNotExecute(() => {
const query = reactive(
useInfiniteQuery({
queryFn: simpleFetcher,
}),
)

if (query.isError) {
const result: Expect<Equal<unknown, typeof query.error>> = true
return result
}
})
})
})
69 changes: 69 additions & 0 deletions packages/vue-query/src/__tests__/useMutation.types.test.tsx
@@ -0,0 +1,69 @@
import { reactive } from 'vue'
import { useMutation } from '../useMutation'
import { doNotExecute, Equal, Expect, successMutator } from './test-utils'

describe('Discriminated union return type', () => {
it('data should be possibly undefined by default', () => {
doNotExecute(() => {
const mutation = reactive(
useMutation({ mutationFn: successMutator<string> }),
)

const result: Expect<Equal<string | undefined, typeof mutation.data>> =
true
return result
})
})

it('data should be defined when mutation is success', () => {
doNotExecute(() => {
const mutation = reactive(
useMutation({ mutationFn: successMutator<string> }),
)

if (mutation.isSuccess) {
const result: Expect<Equal<string, typeof mutation.data>> = true
return result
}
})
})

it('error should be null when mutation is success', () => {
doNotExecute(() => {
const mutation = reactive(
useMutation({ mutationFn: successMutator<string> }),
)

if (mutation.isSuccess) {
const result: Expect<Equal<null, typeof mutation.error>> = true
return result
}
})
})

it('data should be undefined when mutation is loading', () => {
doNotExecute(() => {
const mutation = reactive(
useMutation({ mutationFn: successMutator<string> }),
)

if (mutation.isLoading) {
const result: Expect<Equal<undefined, typeof mutation.data>> = true
return result
}
})
})

it('error should be defined when mutation is error', () => {
doNotExecute(() => {
const mutation = reactive(
useMutation({ mutationFn: successMutator<string> }),
)

if (mutation.isError) {
const result: Expect<Equal<unknown, typeof mutation.error>> = true
return result
}
})
})
})
78 changes: 78 additions & 0 deletions packages/vue-query/src/__tests__/useQuery.types.test.tsx
@@ -0,0 +1,78 @@
import { reactive } from 'vue'
import { useQuery } from '../useQuery'
import { doNotExecute, Equal, Expect, simpleFetcher } from './test-utils'

describe('Discriminated union return type', () => {
it('data should be possibly undefined by default', () => {
doNotExecute(() => {
const query = reactive(
useQuery({
queryFn: simpleFetcher,
}),
)

const result: Expect<Equal<string | undefined, typeof query.data>> = true
return result
})
})

it('data should be defined when query is success', () => {
doNotExecute(() => {
const query = reactive(
useQuery({
queryFn: simpleFetcher,
}),
)

if (query.isSuccess) {
const result: Expect<Equal<string, typeof query.data>> = true
return result
}
})
})

it('error should be null when query is success', () => {
doNotExecute(() => {
const query = reactive(
useQuery({
queryFn: simpleFetcher,
}),
)

if (query.isSuccess) {
const result: Expect<Equal<null, typeof query.error>> = true
return result
}
})
})

it('data should be undefined when query is loading', () => {
doNotExecute(() => {
const query = reactive(
useQuery({
queryFn: simpleFetcher,
}),
)

if (query.isLoading) {
const result: Expect<Equal<undefined, typeof query.data>> = true
return result
}
})
})

it('error should be defined when query is error', () => {
doNotExecute(() => {
const query = reactive(
useQuery({
queryFn: simpleFetcher,
}),
)

if (query.isError) {
const result: Expect<Equal<unknown, typeof query.error>> = true
return result
}
})
})
})
4 changes: 4 additions & 0 deletions packages/vue-query/src/types.ts
Expand Up @@ -92,3 +92,7 @@ export type VueInfiniteQueryObserverOptions<
>[Property]
>
}

export type DistributiveOmit<T, K extends keyof any> = T extends any
? Omit<T, K>
: never
3 changes: 2 additions & 1 deletion packages/vue-query/src/useInfiniteQuery.ts
Expand Up @@ -13,6 +13,7 @@ import type { UseQueryReturnType } from './useBaseQuery'
import type {
WithQueryClientKey,
VueInfiniteQueryObserverOptions,
DistributiveOmit,
} from './types'

export type UseInfiniteQueryOptions<
Expand All @@ -35,7 +36,7 @@ type InfiniteQueryReturnType<TData, TError> = UseQueryReturnType<
TError,
InfiniteQueryObserverResult<TData, TError>
>
export type UseInfiniteQueryReturnType<TData, TError> = Omit<
export type UseInfiniteQueryReturnType<TData, TError> = DistributiveOmit<
InfiniteQueryReturnType<TData, TError>,
'fetchNextPage' | 'fetchPreviousPage' | 'refetch' | 'remove'
> & {
Expand Down
9 changes: 7 additions & 2 deletions packages/vue-query/src/useMutation.ts
Expand Up @@ -16,12 +16,17 @@ import type {
MutationObserverResult,
MutationObserverOptions,
} from '@tanstack/query-core'
import type { WithQueryClientKey, MaybeRef, MaybeRefDeep } from './types'
import type {
WithQueryClientKey,
MaybeRef,
MaybeRefDeep,
DistributiveOmit,
} from './types'
import { MutationObserver } from '@tanstack/query-core'
import { cloneDeepUnref, updateState, isMutationKey } from './utils'
import { useQueryClient } from './useQueryClient'

type MutationResult<TData, TError, TVariables, TContext> = Omit<
type MutationResult<TData, TError, TVariables, TContext> = DistributiveOmit<
MutationObserverResult<TData, TError, TVariables, TContext>,
'mutate' | 'reset'
>
Expand Down
10 changes: 7 additions & 3 deletions packages/vue-query/src/useQuery.ts
Expand Up @@ -8,17 +8,21 @@ import type {
} from '@tanstack/query-core'
import { useBaseQuery } from './useBaseQuery'
import type { UseQueryReturnType as UQRT } from './useBaseQuery'
import type { WithQueryClientKey, VueQueryObserverOptions } from './types'
import type {
WithQueryClientKey,
VueQueryObserverOptions,
DistributiveOmit,
} from './types'

export type UseQueryReturnType<TData, TError> = Omit<
export type UseQueryReturnType<TData, TError> = DistributiveOmit<
UQRT<TData, TError>,
'refetch' | 'remove'
> & {
refetch: QueryObserverResult<TData, TError>['refetch']
remove: QueryObserverResult<TData, TError>['remove']
}

export type UseQueryDefinedReturnType<TData, TError> = Omit<
export type UseQueryDefinedReturnType<TData, TError> = DistributiveOmit<
ToRefs<Readonly<DefinedQueryObserverResult<TData, TError>>>,
'refetch' | 'remove'
> & {
Expand Down

0 comments on commit f6d92a7

Please sign in to comment.