This commit is contained in:
xjccc 2025-02-14 20:07:49 +01:00 committed by GitHub
commit 29095a9c0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 117 additions and 37 deletions

View File

@ -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<T>(
url: string | (() => string),
url: NitroFetchRequest,
options?: UseFetchOptions<T>,
) {
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<T>(
url: string | (() => string),
url: NitroFetchRequest,
options?: UseFetchOptions<T>,
) {
return useFetch<T, FetchError<CustomError>>(url, {
return useFetch<T, CustomError>(url, {
...options,
$fetch: useNuxtApp().$api
})

View File

@ -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<DataT> = KeysOf<DataT>,
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<ResT, DataT>
transform?: _Transform<ResT extends void ? any : ResT, DataT>
/**
* 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<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncDat
*/
export function useAsyncData<
ResT,
NuxtErrorDataT = unknown,
NuxtErrorDataT = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = undefined,
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | undefined>
): AsyncData<PickFrom<DataT, PickKeys> | 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<DataT> = KeysOf<DataT>,
DefaultT = DataT,
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | undefined>
): AsyncData<PickFrom<DataT, PickKeys> | 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<DataT> = KeysOf<DataT>,
DefaultT = undefined,
@ -169,7 +168,7 @@ export function useAsyncData<
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | undefined>
): AsyncData<PickFrom<DataT, PickKeys> | 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<DataT> = KeysOf<DataT>,
DefaultT = DataT,
@ -187,14 +186,14 @@ export function useAsyncData<
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | undefined>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, NuxtErrorDataT | undefined>
export function useAsyncData<
ResT,
NuxtErrorDataT = unknown,
NuxtErrorDataT = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = undefined,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | undefined> {
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, 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<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
const asyncData = { ...nuxtApp._asyncData[key] } as { _default?: unknown } & AsyncData<DataT | DefaultT, NuxtErrorDataT | undefined>
// 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<NuxtErrorDataT>(error) as (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)
asyncData.error.value = createError<NuxtErrorDataT>(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<ResT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
const asyncDataPromise = Promise.resolve(nuxtApp._asyncDataPromises[key]).then(() => asyncData) as AsyncData<ResT, NuxtErrorDataT | undefined>
Object.assign(asyncDataPromise, asyncData)
return asyncDataPromise as AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
return asyncDataPromise as AsyncData<PickFrom<DataT, PickKeys>, NuxtErrorDataT | undefined>
}
/** @since 3.0.0 */
export function useLazyAsyncData<

View File

@ -29,12 +29,13 @@ type ComputedFetchOptions<R extends NitroFetchRequest, M extends AvailableRouter
export interface UseFetchOptions<
ResT,
DataT = ResT,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = undefined,
R extends NitroFetchRequest = string & {},
M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>,
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'watch'>, ComputedFetchOptions<R, M> {
DefaultT = DataT,
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'watch'>, ComputedFetchOptions<ReqT, Method> {
key?: string
$fetch?: typeof globalThis.$fetch
watch?: MultiWatchSources | false
@ -58,7 +59,7 @@ export function useFetch<
DefaultT = undefined,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
opts?: UseFetchOptions<_ResT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | 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 | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
opts?: UseFetchOptions<_ResT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | undefined>
export function useFetch<
ResT = void,
@ -90,7 +91,7 @@ export function useFetch<
DefaultT = undefined,
> (
request: Ref<ReqT> | 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 | (() => ReqT),
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
opts?: Omit<UseFetchOptions<_ResT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | undefined>
export function useLazyFetch<
ResT = void,
@ -209,7 +210,7 @@ export function useLazyFetch<
DefaultT = DataT,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
opts?: Omit<UseFetchOptions<_ResT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | undefined>
export function useLazyFetch<
ResT = void,
@ -222,7 +223,7 @@ export function useLazyFetch<
DefaultT = undefined,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
arg1?: string | Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>,
arg1?: string | Omit<UseFetchOptions<_ResT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT>, '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<string | undefined | Record<string, string>> = [
toValue(opts.method as MaybeRef<string | undefined> | undefined)?.toUpperCase() || 'GET',
toValue(opts.baseURL),

View File

@ -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<T, E = CustomError> (
url: NitroFetchRequest,
options?: UseFetchOptions<T>,
) {
return useFetch<T, E>(url, {
...options,
$fetch: useNuxtApp().$customFetch as typeof $fetch,
})
}
export function useAsyncFetchCustom<T, E = CustomError> (
url: NitroFetchRequest,
options?: AsyncDataOptions<T>,
) {
return useAsyncData<T, E>(() => $fetch(url as string), {
...options,
})
}

View File

@ -0,0 +1,11 @@
export default defineNuxtPlugin(() => {
const fetchCustom = $fetch.create({
baseURL: '',
})
return {
provide: {
fetchCustom,
},
}
})

View File

@ -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<Ref<unknown>>()
expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<unknown> | DefaultAsyncDataErrorValue>>()
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | DefaultAsyncDataErrorValue>>()
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | DefaultAsyncDataErrorValue>>()
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
// backwards compatibility
expectTypeOf(useAsyncData<any, Error>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | DefaultAsyncDataErrorValue>>()
expectTypeOf(useAsyncData<any, NuxtError<string>>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | DefaultAsyncDataErrorValue>>()
@ -502,6 +503,48 @@ describe('composables', () => {
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { default: () => 'test', transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string>>()
})
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<string, CustomError>('/test').error).toEqualTypeOf<Ref<CustomError | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch<string, CustomError>('/test').error).toEqualTypeOf<Ref<CustomError | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData<string, CustomError>('custom-error-type', () => $fetch('/error')).error).toEqualTypeOf<Ref<CustomError | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData<string, CustomError>('custom-error-type', () => $fetch('/error')).error).toEqualTypeOf<Ref<CustomError | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData<string, OtherCustomError>('custom-error-type', () => $fetch('/error')).error).toEqualTypeOf<Ref<OtherCustomError | DefaultAsyncDataValue>>()
expectTypeOf(useFetchCustom<ResT>('/api/hey').data).toEqualTypeOf<Ref<ResT>>()
expectTypeOf(useFetchCustom('/api/hey', { default: () => ({ foo: 'bar', baz: 'baz' }) }).data).toEqualTypeOf<Ref<ResT>>()
expectTypeOf(useFetchCustom('/api/hey', { transform: () => ({ foo: 'bar', baz: 'baz' }) }).data).toEqualTypeOf<Ref<ResT>>()
expectTypeOf(useFetchCustom('/api/hello').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useFetchCustom('/api/hello', { default: () => 'default' }).data).toEqualTypeOf<Ref<string>>()
expectTypeOf(useFetchCustom('/api/hello', { default: () => 'default', transform: () => 'transform' }).data).toEqualTypeOf<Ref<string>>()
expectTypeOf(useFetchCustom<string>('/api/hello', { transform: () => 'transform' }).data).toEqualTypeOf<Ref<string>>()
expectTypeOf(useFetchCustom('/api/hello').error).toEqualTypeOf<Ref<CustomError | DefaultAsyncDataValue>>()
expectTypeOf(useFetchCustom<string, OtherCustomError>('/api/hello').error).toEqualTypeOf<Ref<OtherCustomError | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncFetchCustom<ResT>('/api/hey').data).toEqualTypeOf<Ref<ResT>>()
expectTypeOf(useAsyncFetchCustom('/api/hey', { default: () => ({ foo: 'bar', baz: 'baz' }) }).data).toEqualTypeOf<Ref<ResT>>()
expectTypeOf(useAsyncFetchCustom('/api/hey', { transform: () => ({ foo: 'bar', baz: 'baz' }) }).data).toEqualTypeOf<Ref<ResT>>()
expectTypeOf(useAsyncFetchCustom('/api/hello').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useAsyncFetchCustom('/api/hello', { default: () => 'default' }).data).toEqualTypeOf<Ref<string>>()
expectTypeOf(useAsyncFetchCustom('/api/hello', { default: () => 'default', transform: () => 'transform' }).data).toEqualTypeOf<Ref<string>>()
expectTypeOf(useAsyncFetchCustom<string>('/api/hello', { transform: () => 'transform' }).data).toEqualTypeOf<Ref<string>>()
expectTypeOf(useAsyncFetchCustom('/api/hello').error).toEqualTypeOf<Ref<CustomError | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncFetchCustom<string, OtherCustomError>('/api/hello').error).toEqualTypeOf<Ref<OtherCustomError | DefaultAsyncDataValue>>()
})
it('supports asynchronous transform', () => {
const { data } = useAsyncData('test', () => $fetch('/test') as Promise<{ foo: 'bar' }>, {
async transform (data) {