Search code examples
reactjstypescripttanstackreact-query

How to get conditional return types in react query


I have a common method to use useQuery or useMutation based on the HTTP method as prop. But the return type of this method is containing 'QueryObserverRefetchErrorResult<any, Error>' which doesn't have methods included in useMutation or useQuery.

I tried conditional types such as but the issue was same.

type ResultType<Method extends NTWRK_METHODS> = Method extends NTWRK_METHODS.GET ? NetworkQueryResult : NetworkMutationResult;

My method is


type NetworkQueryResult = UseQueryResult<any, Error>;
type NetworkMutationResult = UseMutationResult<any, Error, void, unknown>;

export function useNetworkQuery(
  props: IUseNetworkCallWithClient,
): NetworkQueryResult | NetworkMutationResult {
  const {queryKey, axiosClient, url, persist, method, params = ''} = props;

  if (method === NTWRK_METHODS.GET) {
    return useQuery({
      queryKey: [queryKey],
      queryFn: async () => {
        const data = await axiosClient.request({
          method: NTWRK_METHODS.GET,
          url: url,
          params: params,
        });
        return data.data;
      },
      enabled: false,
      meta: {persist},
    });
  } else {
    return useMutation({
      mutationKey: [queryKey],
      mutationFn: async () => {
        const data = await axiosClient.request({
          method: method,
          url: url,
          data: params,
        });
        return data.data;
      },
      meta: {persist},
    });
  }
} 

I am calling it as


export const makeNetworkCall = (props: IUseNetworkCallWithoutClient) => {
  const {method, queryKey, url, persist, params = ''} = props;
  const config = {
    axiosClient,
    queryKey: queryKey,
    method: method,
    url: url,
    persist: persist,
    params: params,
  };

  return useNetworkQuery(config);
};

and then

export const useAuthCalls = () => {
  const logIn = () => {
    makeNetworkCall({
      method: NTWRK_METHODS.POST,
      url: '/users',
      queryKey: 'login',
      persist: true,
      params: '',
    });
  };

  return {logIn};
};


Solution

  • Despite what we discussed earlier in the comments I'll give you an answer regarding the typing of your useNetworkQuery function which is pretty much what you're asking in this post.


    What you need here is function overloading to help differentiate what is being returned by useNetworkQuery.

    Simply defining the return type of your function as NetworkQueryResult | NetworkMutationResult will not work well enough because TypeScript won't be able to tell which of these two is the one returned, only that whatever is returned could be either one of these "so treat them as if they were both".

    How about replacing your useNetworkQuery to this?

    export function useNetworkQuery<TData = any, TError extends Error = Error>(props: Exclude<IUseNetworkCallWithClient, "method"> & { method: NTWRK_METHODS.GET; }): UseQueryResult<TData, TError>;
    export function useNetworkQuery<TData = any, TError extends Error = Error, TVariables = any>(props: Exclude<IUseNetworkCallWithClient, "method"> & { method: Exclude<NTWRK_METHODS, NTWRK_METHODS.GET>; }): UseMutationResult<TData, TError, TVariables>;
    export function useNetworkQuery<TData = any, TError extends Error = Error, TVariables = any>(props: IUseNetworkCallWithClient): UseQueryResult<TData, TError> | UseMutationResult<TData, TError, TVariables> {
      const { queryKey, axiosClient, url, persist, method, params = "" } = props;
    
      if (method === NTWRK_METHODS.GET) {
        return useQuery<TData, TError>({
          queryKey: [queryKey],
          queryFn: async () => {
            const data = await axiosClient.request({
              method: NTWRK_METHODS.GET,
              url: url,
              params: params,
            });
            return data.data;
          },
          enabled: false,
          meta: { persist },
        });
      }
    
      return useMutation<TData, TError, TVariables>({
        mutationKey: [queryKey],
        mutationFn: async () => {
          const data = await axiosClient.request({
            method: method,
            url: url,
            data: params,
          });
          return data.data;
        },
        meta: { persist },
      });
    }
    

    A little explanation regarding this:

    1. We overload the function with 2 signatures that change the return type to UseQueryResult<TData, TError> if the given props includes a method property equal to NTWRK_METHODS.GET or changes it to UseMutationResult<TData, TError, TVariables> for any other NTWRK_METHODS that is not NTWRK_METHODS.GET.
    2. We add generics to the function for TData, TError, and TVariables which are generics accepted by the useQuery and useMutation functions. This way, your useNetworkQuery function can keep the correct typing behavior instead of making everything be set to any.

    The previous function should work well enough, but what about makeNetworkCall? If you try to return useNetworkQuery directly you'll notice you'll get an error saying that the implementation call signature is not externally visible. That's because when you overload a function, only the overload signatures are the ones that are taken into account calling the function and the implementation one serves only for the implementation.

    You should then add an extra overload to useNetworkQuery:

    export function useNetworkQuery<TData = any, TError extends Error = Error, TVariables = any>(props: IUseNetworkCallWithClient): UseQueryResult<TData, TError> | UseMutationResult<TData, TError, TVariables>;
    

    Which is basically the same signature as the implementation one.

    Now, you can overload your makeNetworkCall function:

    export function makeNetworkCall<TData = any, TError extends Error = Error>(props: Exclude<IUseNetworkCallWithClient, "method"> & { method: NTWRK_METHODS.GET; }): UseQueryResult<TData, TError>;
    export function makeNetworkCall<TData = any, TError extends Error = Error, TVariables = any>(props: Exclude<IUseNetworkCallWithClient, "method"> & { method: Exclude<NTWRK_METHODS, NTWRK_METHODS.GET>; }): UseMutationResult<TData, TError, TVariables>;
    export function makeNetworkCall<TData = any,TError extends Error = Error,TVariables = any>(props: IUseNetworkCallWithClient): UseQueryResult<TData, TError> | UseMutationResult<TData, TError, TVariables>;
    export function makeNetworkCall(props: IUseNetworkCallWithoutClient) {
      const { method, queryKey, url, persist, params = "" } = props;
      const config = {
        axiosClient,
        queryKey: queryKey,
        method: method,
        url: url,
        persist: persist,
        params: params,
      };
    
      return useNetworkQuery(config);
    }
    

    Finally, you can call your makeNetworkCall hook from your components:

    const getQuery = makeNetworkCall({ method: NTWRK_METHODS.GET }); // Type is UseQueryResult.
    const postQuery = makeNetworkCall({ method: NTWRK_METHODS.POST }); // Type is UseMutationResult.
    

    As a sidenote regarding the useAuthCalls hook...

    How about replacing it with:

    export const useAuthCalls = () => {
      const logInMutation = makeNetworkCall({
        method: NTWRK_METHODS.POST,
        url: "/users",
        queryKey: "login",
        persist: true,
        params: "",
      });
    
      return {
        logIn: logInMutation.mutateAsync,
      };
    };
    

    This way, you're always using the makeNetworkCall hook and only returning the mutateAsync for the logIn call. You should be able to call this from your action handlers with no problem.

    As a suggestion, rename your makeNetworkCall to something that starts with use. The reason for this is to remind you that this is in fact a hook and should be treated as one. Meaning, it should always be called in order and only inside React components.

    If it helps you visualize this better, here's a CodeSandbox that you can check out:

    Edit useNetworkCall