Repository: tannerlinsley/react-query
Pull request #1247 (2020-11-02 17:18:17)

feat: add discriminated unions for query results
Related PR: https://github.com/tannerlinsley/react-query/pull/1108

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/tannerlinsley/react-query/2vohblru1](https://vercel.com/tannerlinsley/react-query/2vohblru1)  
✅ Preview: *In Progress*


tannerlinsley: Heck yes. This is beautiful @boschni. 

Patch:
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 99394e4a5..691cb5c00 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -10,6 +10,7 @@ import {
 import { notifyManager } from './notifyManager'
 import type {
   PlaceholderDataFunction,
+  QueryObserverBaseResult,
   QueryObserverOptions,
   QueryObserverResult,
   QueryOptions,
@@ -390,7 +391,7 @@ export class QueryObserver<
       }
     }
 
-    return {
+    const result: QueryObserverBaseResult<TData, TError> = {
       ...getStatusProps(status),
       data,
       error: state.error,
@@ -398,13 +399,17 @@ export class QueryObserver<
       isFetched: state.dataUpdateCount > 0,
       isFetchedAfterMount: state.dataUpdateCount > this.initialDataUpdateCount,
       isFetching,
+      isLoadingError: status === 'error' && state.dataUpdatedAt === 0,
       isPlaceholderData,
       isPreviousData,
+      isRefetchError: status === 'error' && state.dataUpdatedAt !== 0,
       isStale: this.isStale(),
       refetch: this.refetch,
       remove: this.remove,
       updatedAt,
     }
+
+    return result as QueryObserverResult<TData, TError>
   }
 
   private updateResult(willFetch?: boolean): void {
diff --git a/src/core/types.ts b/src/core/types.ts
index 779b9e4a5..d5747a649 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -217,7 +217,7 @@ export interface FetchPreviousPageOptions extends ResultOptions {
 
 export type QueryStatus = 'idle' | 'loading' | 'error' | 'success'
 
-export interface QueryObserverResult<TData = unknown, TError = unknown> {
+export interface QueryObserverBaseResult<TData = unknown, TError = unknown> {
   data: TData | undefined
   error: TError | null
   failureCount: number
@@ -227,8 +227,10 @@ export interface QueryObserverResult<TData = unknown, TError = unknown> {
   isFetching: boolean
   isIdle: boolean
   isLoading: boolean
-  isPreviousData: boolean
+  isLoadingError: boolean
   isPlaceholderData: boolean
+  isPreviousData: boolean
+  isRefetchError: boolean
   isStale: boolean
   isSuccess: boolean
   refetch: (
@@ -239,8 +241,86 @@ export interface QueryObserverResult<TData = unknown, TError = unknown> {
   updatedAt: number
 }
 
-export interface InfiniteQueryObserverResult<TData = unknown, TError = unknown>
-  extends QueryObserverResult<InfiniteData<TData>, TError> {
+export interface QueryObserverIdleResult<TData = unknown, TError = unknown>
+  extends QueryObserverBaseResult<TData, TError> {
+  data: undefined
+  error: null
+  isError: false
+  isIdle: true
+  isLoading: false
+  isLoadingError: false
+  isRefetchError: false
+  isSuccess: false
+  status: 'idle'
+}
+
+export interface QueryObserverLoadingResult<TData = unknown, TError = unknown>
+  extends QueryObserverBaseResult<TData, TError> {
+  data: undefined
+  error: null
+  isError: false
+  isIdle: false
+  isLoading: true
+  isLoadingError: false
+  isRefetchError: false
+  isSuccess: false
+  status: 'loading'
+}
+
+export interface QueryObserverLoadingErrorResult<
+  TData = unknown,
+  TError = unknown
+> extends QueryObserverBaseResult<TData, TError> {
+  data: undefined
+  error: TError
+  isError: true
+  isIdle: false
+  isLoading: false
+  isLoadingError: true
+  isRefetchError: false
+  isSuccess: false
+  status: 'error'
+}
+
+export interface QueryObserverRefetchErrorResult<
+  TData = unknown,
+  TError = unknown
+> extends QueryObserverBaseResult<TData, TError> {
+  data: TData
+  error: TError
+  isError: true
+  isIdle: false
+  isLoading: false
+  isLoadingError: false
+  isRefetchError: true
+  isSuccess: false
+  status: 'error'
+}
+
+export interface QueryObserverSuccessResult<TData = unknown, TError = unknown>
+  extends QueryObserverBaseResult<TData, TError> {
+  data: TData
+  error: null
+  isError: false
+  isIdle: false
+  isLoading: false
+  isLoadingError: false
+  isRefetchError: false
+  isSuccess: true
+  status: 'success'
+}
+
+export type QueryObserverResult<TData = unknown, TError = unknown> =
+  | QueryObserverIdleResult<TData, TError>
+  | QueryObserverLoadingErrorResult<TData, TError>
+  | QueryObserverLoadingResult<TData, TError>
+  | QueryObserverRefetchErrorResult<TData, TError>
+  | QueryObserverSuccessResult<TData, TError>
+
+export interface InfiniteQueryObserverBaseResult<
+  TData = unknown,
+  TError = unknown
+> extends QueryObserverBaseResult<InfiniteData<TData>, TError> {
   fetchNextPage: (
     options?: FetchNextPageOptions
   ) => Promise<InfiniteQueryObserverResult<TData, TError>>
@@ -253,6 +333,88 @@ export interface InfiniteQueryObserverResult<TData = unknown, TError = unknown>
   isFetchingPreviousPage: boolean
 }
 
+export interface InfiniteQueryObserverIdleResult<
+  TData = unknown,
+  TError = unknown
+> extends InfiniteQueryObserverBaseResult<TData, TError> {
+  data: undefined
+  error: null
+  isError: false
+  isIdle: true
+  isLoading: false
+  isLoadingError: false
+  isRefetchError: false
+  isSuccess: false
+  status: 'idle'
+}
+
+export interface InfiniteQueryObserverLoadingResult<
+  TData = unknown,
+  TError = unknown
+> extends InfiniteQueryObserverBaseResult<TData, TError> {
+  data: undefined
+  error: null
+  isError: false
+  isIdle: false
+  isLoading: true
+  isLoadingError: false
+  isRefetchError: false
+  isSuccess: false
+  status: 'loading'
+}
+
+export interface InfiniteQueryObserverLoadingErrorResult<
+  TData = unknown,
+  TError = unknown
+> extends InfiniteQueryObserverBaseResult<TData, TError> {
+  data: undefined
+  error: TError
+  isError: true
+  isIdle: false
+  isLoading: false
+  isLoadingError: true
+  isRefetchError: false
+  isSuccess: false
+  status: 'error'
+}
+
+export interface InfiniteQueryObserverRefetchErrorResult<
+  TData = unknown,
+  TError = unknown
+> extends InfiniteQueryObserverBaseResult<TData, TError> {
+  data: InfiniteData<TData>
+  error: TError
+  isError: true
+  isIdle: false
+  isLoading: false
+  isLoadingError: false
+  isRefetchError: true
+  isSuccess: false
+  status: 'error'
+}
+
+export interface InfiniteQueryObserverSuccessResult<
+  TData = unknown,
+  TError = unknown
+> extends InfiniteQueryObserverBaseResult<TData, TError> {
+  data: InfiniteData<TData>
+  error: null
+  isError: false
+  isIdle: false
+  isLoading: false
+  isLoadingError: false
+  isRefetchError: false
+  isSuccess: true
+  status: 'success'
+}
+
+export type InfiniteQueryObserverResult<TData = unknown, TError = unknown> =
+  | InfiniteQueryObserverIdleResult<TData, TError>
+  | InfiniteQueryObserverLoadingErrorResult<TData, TError>
+  | InfiniteQueryObserverLoadingResult<TData, TError>
+  | InfiniteQueryObserverRefetchErrorResult<TData, TError>
+  | InfiniteQueryObserverSuccessResult<TData, TError>
+
 export type MutationKey = string | unknown[]
 
 export type MutationStatus = 'idle' | 'loading' | 'success' | 'error'
diff --git a/src/react/types.ts b/src/react/types.ts
index 4e6ab655b..c44dec2f1 100644
--- a/src/react/types.ts
+++ b/src/react/types.ts
@@ -34,14 +34,20 @@ export interface UseInfiniteQueryOptions<
     TQueryData
   > {}
 
-export interface UseBaseQueryResult<TData = unknown, TError = unknown>
-  extends QueryObserverResult<TData, TError> {}
+export type UseBaseQueryResult<
+  TData = unknown,
+  TError = unknown
+> = QueryObserverResult<TData, TError>
 
-export interface UseQueryResult<TData = unknown, TError = unknown>
-  extends UseBaseQueryResult<TData, TError> {}
+export type UseQueryResult<
+  TData = unknown,
+  TError = unknown
+> = UseBaseQueryResult<TData, TError>
 
-export interface UseInfiniteQueryResult<TData = unknown, TError = unknown>
-  extends InfiniteQueryObserverResult<TData, TError> {}
+export type UseInfiniteQueryResult<
+  TData = unknown,
+  TError = unknown
+> = InfiniteQueryObserverResult<TData, TError>
 
 export interface UseMutationOptions<
   TData = unknown,


Test patch:
diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx
index 7807e9275..0e5308280 100644
--- a/src/react/tests/useInfiniteQuery.test.tsx
+++ b/src/react/tests/useInfiniteQuery.test.tsx
@@ -87,8 +87,10 @@ describe('useInfiniteQuery', () => {
       isFetchingPreviousPage: false,
       isIdle: false,
       isLoading: true,
-      isPreviousData: false,
+      isLoadingError: false,
       isPlaceholderData: false,
+      isPreviousData: false,
+      isRefetchError: false,
       isStale: true,
       isSuccess: false,
       refetch: expect.any(Function),
@@ -113,8 +115,10 @@ describe('useInfiniteQuery', () => {
       isFetchingPreviousPage: false,
       isIdle: false,
       isLoading: false,
-      isPreviousData: false,
+      isLoadingError: false,
       isPlaceholderData: false,
+      isPreviousData: false,
+      isRefetchError: false,
       isStale: true,
       isSuccess: true,
       refetch: expect.any(Function),
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index d8714402a..c7e725160 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -116,20 +116,36 @@ describe('useQuery', () => {
     const states: UseQueryResult<string>[] = []
 
     function Page() {
-      const state = useQuery(key, () => 'test')
+      const state = useQuery<string, Error>(key, () => 'test')
 
       states.push(state)
 
-      return (
-        <div>
-          <h1>Status: {state.status}</h1>
-        </div>
-      )
+      if (state.isIdle) {
+        expectType<undefined>(state.data)
+        expectType<null>(state.error)
+        return <span>idle</span>
+      }
+
+      if (state.isLoading) {
+        expectType<undefined>(state.data)
+        expectType<null>(state.error)
+        return <span>loading</span>
+      }
+
+      if (state.isLoadingError) {
+        expectType<undefined>(state.data)
+        expectType<Error>(state.error)
+        return <span>{state.error}</span>
+      }
+
+      expectType<string>(state.data)
+      expectType<Error | null>(state.error)
+      return <span>{state.data}</span>
     }
 
-    const rendered = renderWithClient(queryClient, <Page />)
+    renderWithClient(queryClient, <Page />)
 
-    await waitFor(() => rendered.getByText('Status: success'))
+    await sleep(10)
 
     expect(states[0]).toEqual({
       data: undefined,
@@ -141,8 +157,10 @@ describe('useQuery', () => {
       isFetching: true,
       isIdle: false,
       isLoading: true,
-      isPreviousData: false,
+      isLoadingError: false,
       isPlaceholderData: false,
+      isPreviousData: false,
+      isRefetchError: false,
       isStale: true,
       isSuccess: false,
       refetch: expect.any(Function),
@@ -161,8 +179,10 @@ describe('useQuery', () => {
       isFetching: false,
       isIdle: false,
       isLoading: false,
-      isPreviousData: false,
+      isLoadingError: false,
       isPlaceholderData: false,
+      isPreviousData: false,
+      isRefetchError: false,
       isStale: true,
       isSuccess: true,
       refetch: expect.any(Function),
@@ -211,8 +231,10 @@ describe('useQuery', () => {
       isFetching: true,
       isIdle: false,
       isLoading: true,
-      isPreviousData: false,
+      isLoadingError: false,
       isPlaceholderData: false,
+      isPreviousData: false,
+      isRefetchError: false,
       isStale: true,
       isSuccess: false,
       refetch: expect.any(Function),
@@ -231,8 +253,10 @@ describe('useQuery', () => {
       isFetching: true,
       isIdle: false,
       isLoading: true,
-      isPreviousData: false,
+      isLoadingError: false,
       isPlaceholderData: false,
+      isPreviousData: false,
+      isRefetchError: false,
       isStale: true,
       isSuccess: false,
       refetch: expect.any(Function),
@@ -251,8 +275,10 @@ describe('useQuery', () => {
       isFetching: false,
       isIdle: false,
       isLoading: false,
-      isPreviousData: false,
+      isLoadingError: true,
       isPlaceholderData: false,
+      isPreviousData: false,
+      isRefetchError: false,
       isStale: true,
       isSuccess: false,
       refetch: expect.any(Function),