Implementing RTKQuery queryFn in TypeScript: A comprehensive guide

Important update: The issue is now resolved in RTKQuery v2 after converting to TypeScript. See the original question below.


Brief summary

I am encountering an error while trying to compile the TypeScript code snippet below. Tsc is indicating that the return type from the queryFn function is not satisfactory. Could you please guide me on what might be wrong or missing?

The code snippet

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 message

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<...>'

Temporary fix

Upon reviewing various online resources, I discovered that the code compiles successfully when I remove types from build.query as shown below:

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`);
            },
        }),

However, this workaround sacrifices strong typing throughout my code, making it a somewhat controversial solution.

Answer №1

After some deliberation, I landed on this particular piece of code but I'm not entirely convinced if it's the most efficient approach. The code seems to extend beyond the initial baseQuery call just to meet the compiler's requirements:

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;

Answer №2

By following the guidelines from https://redux-toolkit.js.org/rtk-query/usage-with-typescript#typing-a-basequery, I was able to define a custom type called QueryReturnValue.

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

To handle compilation errors in my queryFn, I utilized the type coercion method as shown below:

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

To simplify this process, I created a utility function to streamline type declarations:

// Utility function accessible to all API slices
export function someHelperForTS<T>(result: QueryReturnValue<unknown, unknown, unknown>) {
    return result as QueryReturnValue<T, unknown, unknown>;
}

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

Although this approach reduces verbosity, it requires declaring the same response type in both build.query and someHelperForTS, which can be cumbersome.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is it possible to generate a Date object from a predetermined string in typescript?

I have a string with values separated by commas, and I'm trying to create a Date object from it. It seems like this is not doable -- can someone verify this and provide a solution if possible? This code snippet doesn't work : let dateString=&a ...

What is the best method for dividing a user interface into several arrays of keys, each grouped by type?

Given a simple structure: structure IPerson { firstName: string; lastName: string; age: number; city: string; favoriteNumber: number; isMarried: boolean; hasDriverLicense: boolean; } How do I create arrays containing keys grouped by data typ ...

Error: The middleware function is invalid and cannot be executed

I am currently facing some challenges while trying to integrate Redux into my ReactJS application. The errors I encountered are: https://i.sstatic.net/UTUnE.png https://i.sstatic.net/VxId8.png Despite having all the necessary dependencies installed, the ...

Vercel seems to be having trouble detecting TypeScript or the "@types/react" package when deploying a Next.js project

Suddenly, my deployment of Next.js to Vercel has hit a snag after the latest update and is now giving me trouble for not having @types/react AND typescript installed. Seems like you're attempting to utilize TypeScript but are missing essential package ...

I am seeking advice on how to create an extension for a generic class in TypeScript specifically as a getter

Recently, I discovered how to create extensions in TypeScript: interface Array<T> { lastIndex(): number } Array.prototype.lastIndex = function (): number { return this.length - 1 } Now, I want to figure out how to make a getter from it. For exam ...

Is there a way to change a typescript enum value into a string format?

I'm working with enums in TypeScript and I need to convert the enum value to a string for passing it to an API endpoint. Can someone please guide me on how to achieve this? Thanks. enum RecordStatus { CancelledClosed = 102830004, Completed = ...

Dealing with Unexpected Timeout Errors and Memory Leaks in Express/Typescript Using Jest, Supertest, and TypeORM

Currently, I am in the process of writing unit tests for an express API. Each test suite initiates a fresh instance of an express server before running the tests. Individually, each test suite runs smoothly without any problems. However, when executed tog ...

Implement a default dropdown menu that displays the current month using Angular and TypeScript

I am looking to implement a dropdown that initially displays the current month. Here is the code snippet I have used: <p-dropdown [options]="months" [filter]="false" filterBy="nombre" [showClear] ...

Using *ngFor in TypeScript code

Hello there, I have a question about performing the logic of *ngFor in typescript. Is it possible to loop through data from my JSON API in the typescript file similar to how *ngFor works in HTML? Any guidance on this matter would be greatly appreciated. ...

RxJS - Only emit if another source does not emit within a specified time frame

Imagine having two observables. Whenever the first one emits, there should be a 2-second pause to check if the other observable emits something within that timeframe. If it does, then no emission should occur. However, if it doesn't emit anything, the ...

Guide on showing a component exclusively for iPads with React and TypeScript

I need help displaying an icon only in the component for iPad devices, and not on other devices. As a beginner in coding for iPads and mobile devices, I am unsure how to achieve this specific requirement for the iPad device. Below is the code snippet tha ...

Unable to locate the module '@vitejs/plugin-react' or its associated type

After creating the project with npm create vite@latest and selecting ts-react, everything seemed to work fine when I ran npm run dev. However, in my vs code editor, I encountered the error message "Cannot find module '@vitejs/plugin-react' or its ...

Adjust the component suppliers based on the @input

If I were to implement a material datepicker with a selection strategy, I would refer to this example There are instances where the selection strategy should not be used. The challenge lies in setting the selection strategy conditionally when it is insta ...

Using @material-ui/core/useScrollTrigger in a Next.js application: a step-by-step guide

I have been researching the Material-UI documentation for useScrollTrigger and attempting to implement it in Next.js to replicate the Elevate App Bar. https://material-ui.com/components/app-bar/#usescrolltrigger-options-trigger import React from "react"; ...

Adjusting the dimensions of the cropper for optimal image cropping

I am currently working on integrating an image cropper component into my project, using the react-cropper package. However, I am facing a challenge in defining a fixed width and height for the cropper box such as "width:200px; height:300px;" impo ...

What is the process for converting BitmapData from Flash into HTML canvas?

Currently facing a significant challenge as I transition from using Flash to JS. The issue lies in handling SOAP returned data, specifically when dealing with images stored as strings that need to be converted into BitmapData for use in Flash. Despite tryi ...

Creating dynamic class fields when ngOnInit() is called in Angular

I am trying to dynamically create variables in a class to store values and use them in ngModel and other places. I understand that I can assign values to variables in the ngOnInit() function like this: export class Component implements OnInit{ name: st ...

Require that the parent FormGroup is marked as invalid unless all nested FormGroups are deemed valid - Implementing a custom

Currently, I am working on an Angular 7 project that involves dynamically generating forms. The structure consists of a parent FormGroup with nested FormGroups of different types. My goal is to have the parentForm marked as invalid until all of the nested ...

Setting up Next Js to display images from external domains

Encountering an issue with my next.config.js file while working on a project with Next.js in TypeScript. This project involves using ThreeJs, @react-three/fiber, and @react-three/drei libraries. Additionally, I need to include images from a specific public ...

The closeOnClickOutside feature seems to be malfunctioning in the angular-2-dropdown-multiselect plugin

I'm currently using 2 angular-2-dropdown-multiselect dropdowns within a bootstarp mega div. The issue I'm facing is that when I click on the dropdown, it opens fine. However, when I click outside of the dropdown, it doesn't close as expected ...