diff --git a/docs/2.guide/4.recipes/3.custom-usefetch.md b/docs/2.guide/4.recipes/3.custom-usefetch.md index bf4b08cdbb..2e98943b91 100644 --- a/docs/2.guide/4.recipes/3.custom-usefetch.md +++ b/docs/2.guide/4.recipes/3.custom-usefetch.md @@ -69,10 +69,11 @@ Wrapping with [`useAsyncData`](/docs/api/composables/use-async-data) **avoid dou Now that `$api` has the logic we want, let's create a `useAPI` composable to replace the usage of `useAsyncData` + `$api`: ```ts [composables/useAPI.ts] +import type { NitroFetchRequest } from 'nitropack' import type { UseFetchOptions } from 'nuxt/app' export function useAPI( - url: string | (() => string), + url: NitroFetchRequest, options?: UseFetchOptions, ) { return useFetch(url, { @@ -92,8 +93,8 @@ const { data: modules } = await useAPI('/modules') If you want to customize the type of any error returned, you can also do so: -```ts -import type { FetchError } from 'ofetch' +```ts twoslash +import type { NitroFetchRequest } from 'nitropack' import type { UseFetchOptions } from 'nuxt/app' interface CustomError { @@ -102,10 +103,10 @@ interface CustomError { } export function useAPI( - url: string | (() => string), + url: NitroFetchRequest, options?: UseFetchOptions, ) { - return useFetch>(url, { + return useFetch(url, { ...options, $fetch: useNuxtApp().$api }) diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 1512786b54..4fe9d079a9 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -3,7 +3,6 @@ import type { MultiWatchSources, Ref } from 'vue' import type { NuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt' import { toArray } from '../utils' -import type { NuxtError } from './error' import { createError } from './error' import { onNuxtReady } from './ready' @@ -42,7 +41,7 @@ export interface AsyncDataOptions< ResT, DataT = ResT, PickKeys extends KeysOf = KeysOf, - DefaultT = undefined, + DefaultT = DataT, > { /** * Whether to fetch on the server side. @@ -68,7 +67,7 @@ export interface AsyncDataOptions< * A function that can be used to alter handler function result after resolving. * Do not use it along with the `pick` option. */ - transform?: _Transform + transform?: _Transform /** * Only pick specified keys in this array from the handler function result. * Do not use it along with the `transform` option. @@ -128,14 +127,14 @@ export type AsyncData = _AsyncData & Promise<_AsyncDat */ export function useAsyncData< ResT, - NuxtErrorDataT = unknown, + NuxtErrorDataT = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = undefined, > ( handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | undefined> +): AsyncData | DefaultT, NuxtErrorDataT | undefined> /** * Provides access to data that resolves asynchronously in an SSR-friendly composable. * See {@link https://nuxt.com/docs/api/composables/use-async-data} @@ -144,14 +143,14 @@ export function useAsyncData< */ export function useAsyncData< ResT, - NuxtErrorDataT = unknown, + NuxtErrorDataT = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = DataT, > ( handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | undefined> +): AsyncData | DefaultT, NuxtErrorDataT | undefined> /** * Provides access to data that resolves asynchronously in an SSR-friendly composable. * See {@link https://nuxt.com/docs/api/composables/use-async-data} @@ -161,7 +160,7 @@ export function useAsyncData< */ export function useAsyncData< ResT, - NuxtErrorDataT = unknown, + NuxtErrorDataT = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = undefined, @@ -169,7 +168,7 @@ export function useAsyncData< key: string, handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | undefined> +): AsyncData | DefaultT, NuxtErrorDataT | undefined> /** * Provides access to data that resolves asynchronously in an SSR-friendly composable. * See {@link https://nuxt.com/docs/api/composables/use-async-data} @@ -179,7 +178,7 @@ export function useAsyncData< */ export function useAsyncData< ResT, - NuxtErrorDataT = unknown, + NuxtErrorDataT = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = DataT, @@ -187,14 +186,14 @@ export function useAsyncData< key: string, handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | undefined> +): AsyncData | DefaultT, NuxtErrorDataT | undefined> export function useAsyncData< ResT, - NuxtErrorDataT = unknown, + NuxtErrorDataT = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = undefined, -> (...args: any[]): AsyncData, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | undefined> { +> (...args: any[]): AsyncData, NuxtErrorDataT | undefined> { const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined if (typeof args[0] !== 'string') { args.unshift(autoKey) } @@ -257,7 +256,7 @@ export function useAsyncData< } // TODO: Else, somehow check for conflicting keys with different defaults or fetcher - const asyncData = { ...nuxtApp._asyncData[key] } as { _default?: unknown } & AsyncData)> + const asyncData = { ...nuxtApp._asyncData[key] } as { _default?: unknown } & AsyncData // Don't expose default function to end user delete asyncData._default @@ -294,7 +293,7 @@ export function useAsyncData< let result = _result as unknown as DataT if (options.transform) { - result = await options.transform(_result) + result = await options.transform(_result as any) } if (options.pick) { result = pick(result as any, options.pick) as DataT @@ -315,7 +314,7 @@ export function useAsyncData< // If this request is cancelled, resolve to the latest request. if ((promise as any).cancelled) { return nuxtApp._asyncDataPromises[key] } - asyncData.error.value = createError(error) as (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) + asyncData.error.value = createError(error) as NuxtErrorDataT asyncData.data.value = unref(options.default!()) asyncData.status.value = 'error' }) @@ -400,10 +399,10 @@ export function useAsyncData< } // Allow directly awaiting on asyncData - const asyncDataPromise = Promise.resolve(nuxtApp._asyncDataPromises[key]).then(() => asyncData) as AsyncData)> + const asyncDataPromise = Promise.resolve(nuxtApp._asyncDataPromises[key]).then(() => asyncData) as AsyncData Object.assign(asyncDataPromise, asyncData) - return asyncDataPromise as AsyncData, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError)> + return asyncDataPromise as AsyncData, NuxtErrorDataT | undefined> } /** @since 3.0.0 */ export function useLazyAsyncData< diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index 5ce5a87d1f..659e013dcc 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -29,12 +29,13 @@ type ComputedFetchOptions = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, + _ResT = ResT extends void ? FetchResult : ResT, + DataT = _ResT, PickKeys extends KeysOf = KeysOf, - DefaultT = undefined, - R extends NitroFetchRequest = string & {}, - M extends AvailableRouterMethod = AvailableRouterMethod, -> extends Omit, 'watch'>, ComputedFetchOptions { + DefaultT = DataT, +> extends Omit, 'watch'>, ComputedFetchOptions { key?: string $fetch?: typeof globalThis.$fetch watch?: MultiWatchSources | false @@ -58,7 +59,7 @@ export function useFetch< DefaultT = undefined, > ( request: Ref | ReqT | (() => ReqT), - opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method> + opts?: UseFetchOptions<_ResT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT> ): AsyncData | DefaultT, ErrorT | undefined> /** * Fetch data from an API endpoint with an SSR-friendly composable. @@ -77,7 +78,7 @@ export function useFetch< DefaultT = DataT, > ( request: Ref | ReqT | (() => ReqT), - opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method> + opts?: UseFetchOptions<_ResT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT> ): AsyncData | DefaultT, ErrorT | undefined> export function useFetch< ResT = void, @@ -90,7 +91,7 @@ export function useFetch< DefaultT = undefined, > ( request: Ref | ReqT | (() => ReqT), - arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, + arg1?: string | UseFetchOptions<_ResT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT>, arg2?: string, ) { const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] @@ -196,7 +197,7 @@ export function useLazyFetch< DefaultT = undefined, > ( request: Ref | ReqT | (() => ReqT), - opts?: Omit, 'lazy'> + opts?: Omit, 'lazy'> ): AsyncData | DefaultT, ErrorT | undefined> export function useLazyFetch< ResT = void, @@ -209,7 +210,7 @@ export function useLazyFetch< DefaultT = DataT, > ( request: Ref | ReqT | (() => ReqT), - opts?: Omit, 'lazy'> + opts?: Omit, 'lazy'> ): AsyncData | DefaultT, ErrorT | undefined> export function useLazyFetch< ResT = void, @@ -222,7 +223,7 @@ export function useLazyFetch< DefaultT = undefined, > ( request: Ref | ReqT | (() => ReqT), - arg1?: string | Omit, 'lazy'>, + arg1?: string | Omit, 'lazy'>, arg2?: string, ) { const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] @@ -240,7 +241,7 @@ export function useLazyFetch< autoKey) } -function generateOptionSegments<_ResT, DataT, DefaultT> (opts: UseFetchOptions<_ResT, DataT, any, DefaultT, any, any>) { +function generateOptionSegments<_ResT, DataT, DefaultT> (opts: UseFetchOptions<_ResT, any, any, _ResT, DataT, any, DefaultT>) { const segments: Array> = [ toValue(opts.method as MaybeRef | undefined)?.toUpperCase() || 'GET', toValue(opts.baseURL), diff --git a/test/fixtures/basic-types/composables/useFetchCustom.ts b/test/fixtures/basic-types/composables/useFetchCustom.ts new file mode 100644 index 0000000000..b02961a58e --- /dev/null +++ b/test/fixtures/basic-types/composables/useFetchCustom.ts @@ -0,0 +1,25 @@ +import type { AsyncDataOptions, UseFetchOptions } from 'nuxt/app' +import type { NitroFetchRequest } from 'nitro/types' + +interface CustomError { + code: string + message: string +} +export function useFetchCustom ( + url: NitroFetchRequest, + options?: UseFetchOptions, +) { + return useFetch(url, { + ...options, + $fetch: useNuxtApp().$customFetch as typeof $fetch, + }) +} + +export function useAsyncFetchCustom ( + url: NitroFetchRequest, + options?: AsyncDataOptions, +) { + return useAsyncData(() => $fetch(url as string), { + ...options, + }) +} diff --git a/test/fixtures/basic-types/plugins/fetchCustom.ts b/test/fixtures/basic-types/plugins/fetchCustom.ts new file mode 100644 index 0000000000..340e0d37f4 --- /dev/null +++ b/test/fixtures/basic-types/plugins/fetchCustom.ts @@ -0,0 +1,11 @@ +export default defineNuxtPlugin(() => { + const fetchCustom = $fetch.create({ + baseURL: '', + }) + + return { + provide: { + fetchCustom, + }, + } +}) diff --git a/test/fixtures/basic-types/types.ts b/test/fixtures/basic-types/types.ts index 4ed752a877..d5da08b71d 100644 --- a/test/fixtures/basic-types/types.ts +++ b/test/fixtures/basic-types/types.ts @@ -9,6 +9,7 @@ import type { NitroRouteRules } from 'nitro/types' import type { AppConfig, RuntimeValue, UpperSnakeCase } from 'nuxt/schema' import { defineNuxtModule } from 'nuxt/kit' import { defineNuxtConfig } from 'nuxt/config' +import { useAsyncFetchCustom, useFetchCustom } from './composables/useFetchCustom' import { callWithNuxt, isVue3 } from '#app' import type { NuxtError } from '#app' import type { NavigateToOptions } from '#app/composables/router' @@ -63,8 +64,8 @@ describe('API routes', () => { expectTypeOf(useAsyncData('api-other', () => $fetch('/api/other')).data).toEqualTypeOf>() expectTypeOf(useAsyncData('api-generics', () => $fetch('/test')).data).toEqualTypeOf>() - expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf | DefaultAsyncDataErrorValue>>() - expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf | DefaultAsyncDataErrorValue>>() + expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf>() + expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf>() // backwards compatibility expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf>() expectTypeOf(useAsyncData>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf | DefaultAsyncDataErrorValue>>() @@ -502,6 +503,48 @@ describe('composables', () => { expectTypeOf(useLazyAsyncData(() => $fetch('/test'), { default: () => 'test', transform: () => 'transformed' }).data).toEqualTypeOf>() }) + it('correct types when using custom error type', () => { + interface ResT { + foo: string + baz: string + } + interface CustomError { + message: string + code: string + } + + interface OtherCustomError { + message: string + code: number + } + + expectTypeOf(useFetch('/test').error).toEqualTypeOf>() + expectTypeOf(useLazyFetch('/test').error).toEqualTypeOf>() + expectTypeOf(useAsyncData('custom-error-type', () => $fetch('/error')).error).toEqualTypeOf>() + expectTypeOf(useLazyAsyncData('custom-error-type', () => $fetch('/error')).error).toEqualTypeOf>() + expectTypeOf(useLazyAsyncData('custom-error-type', () => $fetch('/error')).error).toEqualTypeOf>() + + expectTypeOf(useFetchCustom('/api/hey').data).toEqualTypeOf>() + expectTypeOf(useFetchCustom('/api/hey', { default: () => ({ foo: 'bar', baz: 'baz' }) }).data).toEqualTypeOf>() + expectTypeOf(useFetchCustom('/api/hey', { transform: () => ({ foo: 'bar', baz: 'baz' }) }).data).toEqualTypeOf>() + expectTypeOf(useFetchCustom('/api/hello').data).toEqualTypeOf>() + expectTypeOf(useFetchCustom('/api/hello', { default: () => 'default' }).data).toEqualTypeOf>() + expectTypeOf(useFetchCustom('/api/hello', { default: () => 'default', transform: () => 'transform' }).data).toEqualTypeOf>() + expectTypeOf(useFetchCustom('/api/hello', { transform: () => 'transform' }).data).toEqualTypeOf>() + expectTypeOf(useFetchCustom('/api/hello').error).toEqualTypeOf>() + expectTypeOf(useFetchCustom('/api/hello').error).toEqualTypeOf>() + + expectTypeOf(useAsyncFetchCustom('/api/hey').data).toEqualTypeOf>() + expectTypeOf(useAsyncFetchCustom('/api/hey', { default: () => ({ foo: 'bar', baz: 'baz' }) }).data).toEqualTypeOf>() + expectTypeOf(useAsyncFetchCustom('/api/hey', { transform: () => ({ foo: 'bar', baz: 'baz' }) }).data).toEqualTypeOf>() + expectTypeOf(useAsyncFetchCustom('/api/hello').data).toEqualTypeOf>() + expectTypeOf(useAsyncFetchCustom('/api/hello', { default: () => 'default' }).data).toEqualTypeOf>() + expectTypeOf(useAsyncFetchCustom('/api/hello', { default: () => 'default', transform: () => 'transform' }).data).toEqualTypeOf>() + expectTypeOf(useAsyncFetchCustom('/api/hello', { transform: () => 'transform' }).data).toEqualTypeOf>() + expectTypeOf(useAsyncFetchCustom('/api/hello').error).toEqualTypeOf>() + expectTypeOf(useAsyncFetchCustom('/api/hello').error).toEqualTypeOf>() + }) + it('supports asynchronous transform', () => { const { data } = useAsyncData('test', () => $fetch('/test') as Promise<{ foo: 'bar' }>, { async transform (data) {