Search code examples
typescriptaxiostypescript-generics

Awaited<R> is not being unwrapped to R with response object


I'm returning an AxiosResponse object from a call to axios.get in a generic function. That object has the shape:

interface AxiosResponse<T = any, D = any> {
  data: T;
  ...
}

The get call has this type:

get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;

I'm hours into digging for an answer to fix this error:

"Property 'data' does not exist on type 'Awaited<R>

In this code (error location marked):

export const fetchWithRetry = async <
  T = any,
  Q = Record<string, string>,
  R = AxiosResponse<T>
>(
  route: string,
  queryParams: Partial<Q> = {},
) => {
  const response = await fetchResponseWithRetry<T, Q, R>(route, queryParams)
  return response.data; // <= ERROR IS HERE
};

export const fetchResponseWithRetry = async <
  T = any,
  Q = Record<string, string>,
  R = AxiosResponse<T>,
>(
  route: string,
  queryParams: Partial<Q> = {},
) => {
  try {
    const options: AxiosRequestConfig = {
      headers: {
        "Content-Type": "application/json",
      },
      params: <Q>{ ...queryParams },
    };
    return (await axios.get<T, R, unknown>(route, options));
  } catch (error) {
    if (error instanceof Error) {
      buildLogger(`${error.message}`);
      if (error instanceof AxiosError) buildLogger(`${error.request.url}`);
    }
  }
  throw new Error(`This should never be reached.`);
};

I understand that the Awaited<R> type was added in 4.5. I understand from other answers that the Awaited type should be unwrapped to R (from the accepted answer on that question):

If the type T you are awaiting is a Promise<U> where U isn't a Promise of any kind, then Awaited<T> is the same as U

How do I fix this?


Solution

  • The problem here is that you're customisation of the R type means the response could literally be anything so Typescript cannot guarantee it has a data property.

    There's really no need to provide generics for anything other than the T response data type.

    This code works without any errors and also does away with a few other small issues

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    export const fetchWithRetry = async <T = any,>(
      route: string,
      queryParams: Record<string, string> = {},
    ) => {
      const response = await fetchResponseWithRetry<T>(route, queryParams);
      return response.data;
    };
    
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    export const fetchResponseWithRetry = async <T = any,>(
      route: string,
      queryParams: Record<string, string> = {},
    ) => {
      try {
        // GET requests do not have a body so no content-type required
        const options: AxiosRequestConfig = {
          params: queryParams,
        };
        return await axios.get<T>(route, options);
      } catch (error) {
        if (error instanceof Error) {
          buildLogger(`${error.message}`);
          if (axios.isAxiosError(error)) {
            // use the provided type guard method
            buildLogger(`${error.request.url}`);
          }
        }
      }
      throw new Error(`This should never be reached.`);
    };
    

    If you really wanted to keep the R generic, you should use extends instead of a default value.

    R extends { data: T } = AxiosResponse<T>