nitro icon indicating copy to clipboard operation
nitro copied to clipboard

TS error `Excessive stack depth comparing types` when trying to wrap `$fetch`

Open Baloche opened this issue 3 years ago • 16 comments

Environment


  • Operating System: Darwin
  • Node Version: v16.15.1
  • Nuxt Version: 3.0.0-rc.8
  • Package Manager: [email protected]
  • Builder: vite
  • User Config: runtimeConfig, css, vite, typescript, modules, unocss
  • Runtime Modules: @unocss/nuxt@^0.44.7, @vueuse/[email protected]
  • Build Modules: -

Reproduction

export const wrappedFetch: typeof $fetch = (request, opts?) => $fetch(request, opts)
wrappedFetch.raw = (request, opts?) => $fetch.raw(request, opts)

Describe the bug

In Nuxt 3.0.0-rc.8, when trying to wrap the $fetch method in a custom method with the same type signature (to set default options for example), typescript throw an Excessive stack depth comparing types error, forcing me to add // @ts-ignore on the line before.

This only happens for $fetch. useFetch can be wrapped without errors.

Is it normal ?

Additional context

No response

Logs

No response

Baloche avatar Sep 05 '22 13:09 Baloche

(nuxt 3.2)

The problem is related to AvailableRouterMethod<R> type called by method attribute in NitroFetchOptions type definition from nitropack package.

interface NitroFetchOptions<R extends NitroFetchRequest> extends FetchOptions {
    method?: Uppercase<AvailableRouterMethod<R>> | AvailableRouterMethod<R>;
}

interface $Fetch<DefaultT = unknown, DefaultR extends NitroFetchRequest = NitroFetchRequest> {
    <T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(request: R, opts?: O): Promise<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>>;
    raw<T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(request: R, opts?: O): Promise<FetchResponse<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>>>;
    create<T = DefaultT, R extends NitroFetchRequest = DefaultR>(defaults: FetchOptions): $Fetch<T, R>;
}

ozum avatar Feb 08 '23 13:02 ozum

@pi0 can you take a look at this please? Can still reproduce in latest versions.

daniluk4000 avatar Jun 16 '23 18:06 daniluk4000

Still having this issue with:

import type { AvailableRouterMethod, NitroFetchRequest, NitroFetchOptions } from 'nitropack';

const $fetchApi = async <
  ResT = void,
  ReqT extends NitroFetchRequest = NitroFetchRequest,
  Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>
>(
  req: ReqT, opts?: NitroFetchOptions<ReqT>
)

ElYaiko avatar Jul 16 '23 00:07 ElYaiko

Same here, problem still present:

const result:CasirestToken = await $fetch(settings.casirest.dev.endpoint + '/tokens', {
	method: 'POST',
	headers: {
		'X-API-Key': settings.casirest.dev.apikey
	},
	body: creds
});
console.log(result);

gerritvanaaken avatar Jul 21 '23 16:07 gerritvanaaken

/cc @danielroe

pi0 avatar Jul 24 '23 09:07 pi0

It is the same when trying to do with useFetch or useAsyncData. For those who stuck with useFetch, I managed it as this:

export const useFetchApi = <T>(
  ...[request, options]: Parameters<typeof useFetch<T>>
): ReturnType<typeof useFetch<T>> => {
  return useFetch<T>(request, {
    ...options,
    async onRequest({ options }) {
      const accessToken = await useAuthStore().getAccessToken()

      if (accessToken) {
        const headers = new Headers(options.headers)

        headers.set('Accept', 'application/json')
        headers.set('Authorization', `Bearer ${accessToken}`)

        options.headers = headers
      }
    },
  })
}

maxdzin avatar Jul 24 '23 09:07 maxdzin

Finally found this workaround for $fetch, if anyone is interested :

import type {
  NitroFetchRequest,
} from "nitropack";

export function $api<
  T = unknown,
  R extends NitroFetchRequest = NitroFetchRequest
>(
  request: Parameters<typeof $fetch<T, R>>[0],
  opts?: Partial<Parameters<typeof $fetch<T, R>>[1]>
) {
  return $fetch<T, R>(request, {
    // add your custom options here
    ...opts,
  });
}

lacorde avatar Aug 24 '23 08:08 lacorde

having the same issue with that code:

const { data: cities, error: cityError } = await useAsyncData(
  "posts",
  () =>
    // @ts-ignore
    $fetch("/api/tool/data/city/", {
      query: { departmentcode: departmentsSelect.value?.code },
    }),
  {
    immediate: false,
    watch: [departmentsSelect],
  }
);

if (cityError.value) {
  errorsHandler(cityError.value);
}

my error:

image

jervalles avatar Sep 06 '23 22:09 jervalles

having the same issue with that code:

const { data: cities, error: cityError } = await useAsyncData(
  "posts",
  () =>
    // @ts-ignore
    $fetch("/api/tool/data/city/", {
      query: { departmentcode: departmentsSelect.value?.code },
    }),
  {
    immediate: false,
    watch: [departmentsSelect],
  }
);

if (cityError.value) {
  errorsHandler(cityError.value);
}

my error:

image

For this scenario, have you tried using useFetch instead of useAsyncData ? You can pass query parameters too.

Hebilicious avatar Sep 07 '23 11:09 Hebilicious

having the same issue with that code:

const { data: cities, error: cityError } = await useAsyncData(
  "posts",
  () =>
    // @ts-ignore
    $fetch("/api/tool/data/city/", {
      query: { departmentcode: departmentsSelect.value?.code },
    }),
  {
    immediate: false,
    watch: [departmentsSelect],
  }
);

if (cityError.value) {
  errorsHandler(cityError.value);
}

my error: image

For this scenario, have you tried using useFetch instead of useAsyncData ? You can pass query parameters too.

yes I tried. Same error

jervalles avatar Sep 07 '23 23:09 jervalles

@jervalles try this:

    $fetch<unknown>("/api/tool/data/city/", {
      query: { departmentcode: departmentsSelect.value?.code },
    }),

Worked for me on Nuxt.

ssotomayor avatar Sep 20 '23 08:09 ssotomayor

finally I wrap $fetch on nuxt3

// types/index.d.ts

type FetchSchemaApi<DefaultApiResponse = unknown> = {
  statusCode: number
  data: DefaultApiResponse
}

declare module 'nitropack' {
  interface $Fetch<DefaultT = unknown, DefaultR extends NitroFetchRequest = NitroFetchRequest> {
    <T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(request: R, opts?: O): Promise<TypedInternalResponse<R, FetchSchemaApi<T>, ExtractedRouteMethod<R, O>>>;
    raw<T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(request: R, opts?: O): Promise<FetchResponse<TypedInternalResponse<R, FetchSchemaApi<T>, ExtractedRouteMethod<R, O>>>>;
    create<T = DefaultT, R extends NitroFetchRequest = DefaultR>(defaults: FetchOptions): $Fetch<FetchSchemaApi<T>, R>;
  }
}
Screenshot 2024-01-07 at 13 43 41

kenhyuwa avatar Jan 07 '24 06:01 kenhyuwa

Im on Nuxt 3.9.1 and Vue 3.4.5, I have this error when I use $fetch inside a function. (ignore the redeclare error, just for example) image

const debounceFn = useDebounceFn(async () => {
  results.value = await $fetch('/api/address/search', {
    query: {
      q: temp.value,
    },
  })
}, 750)

If I move $fetch outside, it doesn't complain image

results.value = await $fetch('/api/address/search', {
  query: {
    q: temp.value,
  },
})

beejaz avatar Jan 10 '24 20:01 beejaz

Have the same problem when using $fetch. Strangely I can't seem to reproduce it in a minimal reproduction, so it might have something to do with a large return type or the Nuxt project structure I have. This is a $fetch GET call without any options, body or query.

Using Nuxt 3.11.2.

Ragura avatar Apr 12 '24 16:04 Ragura

Yes, this issue is happening in big projects. The only workaround I found and using for now is to specify certain return type:

$fetch<{
  item: ISomeItemType
  ...
}>(...)

Using the latest Nuxt 3.11.2

maxdzin avatar Apr 13 '24 12:04 maxdzin

have same issue :( image

image

workaround by @kenhyuwa work for me, but typecheck of return type not work anymore

Nuxt project info:                                                                                                                                               07:58:42  

------------------------------
- Operating System: Windows_NT
- Node Version:     v20.7.0
- Nuxt Version:     3.11.2
- CLI Version:      3.11.1
- Nitro Version:    2.9.6
- Package Manager:  [email protected]
- Builder:          -
- User Config:      typescript, devtools, app, build, modules, cron, pinia, experimental, auth
- Runtime Modules:  @unocss/[email protected], @bg-dev/[email protected], @nuxt/[email protected], @pinia/[email protected], @vueuse/[email protected], @sidebase/[email protected], [email protected]

mttzzz avatar Apr 17 '24 04:04 mttzzz

This issue is still present. The workaround of providing a type to $fetch(), like $fetch<myType>() works, but of course this isn't ideal because we lose automatic type inference. This seems to point towards this automatic inference somehow tripping typescript.

I have tried again and again to reproduce this issue in a minimal app but it just doesn't show up there. It also only shows up for routes of a specific route folder. These routes do deal with numerous deep types inside them, returning large complex JSON data.

I am well aware that this is very difficult to fix if you can't make the error appear anywhere on your end, so it seems we're stuck providing the type hint ourselves for the time being.

Ragura avatar Jul 15 '24 13:07 Ragura

this "Excessive stack" error comes and goes, but it's more likely to happen with default GET requests. Explicitly setting method: 'GET' seems to resolve the issue.

Steps to Reproduce:

/server/api
  └── /something
        ├── index.get.ts
        └── [id].put.ts

Make a default GET request using $fetch, then another request with a different method (e.g., PUT) with param:

const getSomething = $fetch('/api/something')  // Likely to cause "Excessive stack" error
const putSomething = $fetch(/api/something/${id}, { method: 'PUT' })

How I got rid of it:

const getSomething = $fetch('/api/something', { method: 'GET' }) // Explicitly setting GET
const putSomething = $fetch(/api/something/${id}, { method: 'PUT' })

fetch

minkoonim avatar Jul 19 '24 07:07 minkoonim