How to use RTKQuery queryFn with TypeScript
Asked Answered
U

2

5

NOTE: This now works fine in RTKQuery v2 as it has been rewritten to TypeScript. Original question below.


Short description

I'm trying to compile below piece of TypeScript code. Tsc isn't satisfied with my return type from the queryFn function. Am I doing something wrong or am I missing something?

The code

import { FetchBaseQueryError } from '@reduxjs/toolkit/dist/query/fetchBaseQuery';
import { baseApi } from '../../../api/baseApi';
import { RootState } from '../../../redux/store';
import { UserResponse } from './userApiTypes';

export const extendedApi = baseApi.injectEndpoints({
    endpoints: build => ({
        getUser: build.query<UserResponse, void>({
            queryFn: async (arg, api, extraOptions, baseQuery) => {
                const state = api.getState() as RootState;

                if (!state.auth.loginToken && !state.auth.refreshToken)
                    return { error: { error: `UNAUTHORIZED`, status: `CUSTOM_ERROR` } as FetchBaseQueryError };

                return await baseQuery(`/user/me`);
            },
        }),
    }),
});

export const { useGetUserQuery, useLazyGetUserQuery } = extendedApi;

The error

Type '(arg: void, api: BaseQueryApi, extraOptions: any, baseQuery: (arg: string | FetchArgs) => Promise<QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>>) => Promise<...>' is not assignable to type '(arg: void, api: BaseQueryApi, extraOptions: any, baseQuery: (arg: string | FetchArgs) => Promise<QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>>) => MaybePromise<...>'.
  Type 'Promise<QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>>' is not assignable to type 'MaybePromise<QueryReturnValue<UserResponse, FetchBaseQueryError, unknown>>'.
    Type 'Promise<QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>>' is not assignable to type 'PromiseLike<QueryReturnValue<UserResponse, FetchBaseQueryError, unknown>>'.
      Types of property 'then' are incompatible.
        Type '<TResult1 = QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>, TResult2 = never>(onfulfilled?: ((value: QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>) => TResult1 | PromiseLike<...>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<...>) | ... 1 more ......' is not assignable to type '<TResult1 = QueryReturnValue<UserResponse, FetchBaseQueryError, unknown>, TResult2 = never>(onfulfilled?: ((value: QueryReturnValue<UserResponse, FetchBaseQueryError, unknown>) => TResult1 | PromiseLike<...>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<...>) | ... 1 more ... | undefined...'.
          Types of parameters 'onfulfilled' and 'onfulfilled' are incompatible.
            Types of parameters 'value' and 'value' are incompatible.
              Type 'QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>' is not assignable to type 'QueryReturnValue<UserResponse, FetchBaseQueryError, unknown>'.
                Type '{ error?: undefined; data: unknown; meta?: FetchBaseQueryMeta | undefined; }' is not assignable to type 'QueryReturnValue<UserResponse, FetchBaseQueryError, unknown>'.
                  Type '{ error?: undefined; data: unknown; meta?: FetchBaseQueryMeta | undefined; }' is not assignable to type '{ error?: undefined; data: UserResponse; meta?: unknown; }'.
                    Types of property 'data' are incompatible.
                      Type 'unknown' is not assignable to type 'UserResponse'.ts(2322)
endpointDefinitions.d.ts(37, 5): The expected type comes from property 'queryFn' which is declared here on type 'Omit<EndpointDefinitionWithQuery<void, (args: string | FetchArgs, api: BaseQueryApi, extraOptions: any) => Promise<QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>>, UserResponse> & { ...; } & { ...; } & QueryExtraOptions<...>, "type"> | Omit<...>'

Bad workaround

Looking at the examples on the internet I am able to compile this code when I remove types from build.query to make this enpoint like this:

getUser: build.query({ // changed here
            queryFn: async (arg, api, extraOptions, baseQuery) => {
                const state = api.getState() as RootState;

                if (!state.auth.loginToken && !state.auth.refreshToken)
                    return { error: { error: `UNAUTHORIZED`, status: `CUSTOM_ERROR` } as FetchBaseQueryError };

                return await baseQuery(`/user/me`);
            },
        }),

But then I lose strong typing everywhere in my code which makes this workaround at least controversial.

Urease answered 17/11, 2022 at 9:26 Comment(0)
U
5

I ended up with this code but I'm not sure if that's the recommended approach. There is a lot going on after the baseQuery is called just to satisfy the compiler:

import { baseApi } from '../../../api/baseApi';
import { BaseResponse } from '../../../api/baseApiTypes';
import { RootState } from '../../../redux/store';
import { UserResponse } from './userApiTypes';

export const extendedApi = baseApi.injectEndpoints({
  endpoints: build => ({
    getUser: build.query<UserResponse, void>({
      queryFn: async (arg, api, extraOptions, baseQuery) => {
        const state = api.getState() as RootState;

        if (!state.auth.tokens)
          return { error: { error: `UNAUTHORIZED`, status: `CUSTOM_ERROR` } };

        const result = await baseQuery(`/user/me`);

        if (result.error)
          return { error: result.error };

        const baseResponseWithUser = result.data as BaseResponse<UserResponse>;

        if (!baseResponseWithUser.data)
          return { error: { error: `UNKNOWN_ERROR`, status: `CUSTOM_ERROR` } };

        return { data: baseResponseWithUser.data };
      },
    }),
  }),
});

export const { useGetUserQuery, useLazyGetUserQuery } = extendedApi;
Urease answered 21/11, 2022 at 14:18 Comment(2)
What is BaseResponse?Earleenearlene
@TalhaJunaid BaseResponse is the type my API returns from every endpoint. It can for example be { isSuccess: boolean, payload: T }Urease
B
5

I followed https://redux-toolkit.js.org/rtk-query/usage-with-typescript#typing-a-basequery so I have a QueryReturnValue type

export type QueryReturnValue<T = unknown, E = unknown, M = unknown> =
| {
  error: E
  data?: undefined
  meta?: M
}
| {
  error?: undefined
  data: T
  meta?: M
}

Then inside a queryFn I do the following which resolves TS compilation errors.

const result = await baseQuery(`/user/me`);
return result as QueryReturnValue<UserResponse, unknown, unknown>;

What I then did was create a helper function to make this less verbose.

// Somewhere all api slices can import
export function someHelperForTS<T>(result: QueryReturnValue<unknown, unknown, unknown>) {
    return result as QueryReturnValue<T, unknown, unknown>;
}

// Within queryFn
const result = await baseQuery(`/user/me`);
return someHelperForTS<UserResponse>(result);

I don't love it as I have to declare the same response type in both build.query (build.query<UserResponse) and someHelperForTS (someHelperForTS<UserResponse>).

Bromic answered 3/3, 2023 at 15:4 Comment(1)
This is fantastic! One modification I had to make to your solution to resolve the TS compilation error on my instance is to type the QueryReturnValue as QueryReturnValue<UserResponse, FetchBaseQueryError, object | undefined)> instead. The compiler was not accepting unknown for Error and Meta types .Taste

© 2022 - 2024 — McMap. All rights reserved.