fix(nuxt): ensure error in useAsyncData has correct type (#24396)

This commit is contained in:
Damian Głowala 2023-12-14 13:41:40 +01:00 committed by GitHub
parent 4e0d2c073f
commit 72c8503236
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 55 additions and 29 deletions

View File

@ -2,6 +2,7 @@ import { getCurrentInstance, onBeforeMount, onServerPrefetch, onUnmounted, ref,
import type { Ref, WatchSource } from 'vue' import type { Ref, WatchSource } from 'vue'
import type { NuxtApp } from '../nuxt' import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt'
import type { NuxtError} from './error';
import { createError } from './error' import { createError } from './error'
import { onNuxtReady } from './ready' import { onNuxtReady } from './ready'
@ -82,27 +83,27 @@ const isDefer = (dedupe?: boolean | 'cancel' | 'defer') => dedupe === 'defer' ||
export function useAsyncData< export function useAsyncData<
ResT, ResT,
DataE = Error, NuxtErrorDataT = unknown,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = null,
> ( > (
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT> options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null> ): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
export function useAsyncData< export function useAsyncData<
ResT, ResT,
DataE = Error, NuxtErrorDataT = unknown,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = DataT, DefaultT = DataT,
> ( > (
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT> options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null> ): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
export function useAsyncData< export function useAsyncData<
ResT, ResT,
DataE = Error, NuxtErrorDataT = unknown,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = null,
@ -110,10 +111,10 @@ export function useAsyncData<
key: string, key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT> options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null> ): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
export function useAsyncData< export function useAsyncData<
ResT, ResT,
DataE = Error, NuxtErrorDataT = unknown,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = DataT, DefaultT = DataT,
@ -121,14 +122,14 @@ export function useAsyncData<
key: string, key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT> options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null> ): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
export function useAsyncData< export function useAsyncData<
ResT, ResT,
DataE = Error, NuxtErrorDataT = unknown,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, DataE | null> { > (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null> {
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
if (typeof args[0] !== 'string') { args.unshift(autoKey) } if (typeof args[0] !== 'string') { args.unshift(autoKey) }
@ -177,7 +178,7 @@ export function useAsyncData<
} }
// TODO: Else, somehow check for conflicting keys with different defaults or fetcher // TODO: Else, somehow check for conflicting keys with different defaults or fetcher
const asyncData = { ...nuxt._asyncData[key] } as AsyncData<DataT | DefaultT, DataE> const asyncData = { ...nuxt._asyncData[key] } as AsyncData<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
asyncData.refresh = asyncData.execute = (opts = {}) => { asyncData.refresh = asyncData.execute = (opts = {}) => {
if (nuxt._asyncDataPromises[key]) { if (nuxt._asyncDataPromises[key]) {
@ -224,7 +225,7 @@ export function useAsyncData<
// If this request is cancelled, resolve to the latest request. // If this request is cancelled, resolve to the latest request.
if ((promise as any).cancelled) { return nuxt._asyncDataPromises[key] } if ((promise as any).cancelled) { return nuxt._asyncDataPromises[key] }
asyncData.error.value = createError(error) as DataE asyncData.error.value = createError<NuxtErrorDataT>(error) as (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)
asyncData.data.value = unref(options.default!()) asyncData.data.value = unref(options.default!())
asyncData.status.value = 'error' asyncData.status.value = 'error'
}) })
@ -295,10 +296,10 @@ export function useAsyncData<
} }
// Allow directly awaiting on asyncData // Allow directly awaiting on asyncData
const asyncDataPromise = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => asyncData) as AsyncData<ResT, DataE> const asyncDataPromise = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => asyncData) as AsyncData<ResT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
Object.assign(asyncDataPromise, asyncData) Object.assign(asyncDataPromise, asyncData)
return asyncDataPromise as AsyncData<PickFrom<DataT, PickKeys>, DataE> return asyncDataPromise as AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
} }
export function useLazyAsyncData< export function useLazyAsyncData<
ResT, ResT,

View File

@ -1,44 +1,65 @@
import type { H3Error } from 'h3' import type { H3Error } from 'h3'
import { createError as _createError } from 'h3' import { createError as createH3Error } from 'h3'
import { toRef } from 'vue' import { toRef } from 'vue'
import { useNuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt'
import { useRouter } from './router' import { useRouter } from './router'
export const NUXT_ERROR_SIGNATURE = '__nuxt_error'
export const useError = () => toRef(useNuxtApp().payload, 'error') export const useError = () => toRef(useNuxtApp().payload, 'error')
export interface NuxtError extends H3Error {} export interface NuxtError<DataT = unknown> extends H3Error<DataT> {}
export const showError = (_err: string | Error | Partial<NuxtError>) => { export const showError = <DataT = unknown>(
const err = createError(_err) error: string | Error | Partial<NuxtError<DataT>>
) => {
const nuxtError = createError<DataT>(error)
try { try {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const error = useError() const error = useError()
if (import.meta.client) { if (import.meta.client) {
nuxtApp.hooks.callHook('app:error', err) nuxtApp.hooks.callHook('app:error', nuxtError)
} }
error.value = error.value || err
error.value = error.value || nuxtError
} catch { } catch {
throw err throw nuxtError
} }
return err return nuxtError
} }
export const clearError = async (options: { redirect?: string } = {}) => { export const clearError = async (options: { redirect?: string } = {}) => {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const error = useError() const error = useError()
nuxtApp.callHook('app:error:cleared', options) nuxtApp.callHook('app:error:cleared', options)
if (options.redirect) { if (options.redirect) {
await useRouter().replace(options.redirect) await useRouter().replace(options.redirect)
} }
error.value = null error.value = null
} }
export const isNuxtError = (err?: string | object): err is NuxtError => !!(err && typeof err === 'object' && ('__nuxt_error' in err)) export const isNuxtError = <DataT = unknown>(
error?: string | object
): error is NuxtError<DataT> => (
!!error && typeof error === 'object' && NUXT_ERROR_SIGNATURE in error
)
export const createError = (err: string | Partial<NuxtError>): NuxtError => { export const createError = <DataT = unknown>(
const _err: NuxtError = _createError(err) error: string | Partial<NuxtError<DataT>>
;(_err as any).__nuxt_error = true ) => {
return _err const nuxtError: NuxtError<DataT> = createH3Error<DataT>(error)
Object.defineProperty(nuxtError, NUXT_ERROR_SIGNATURE, {
value: true,
configurable: false,
writable: false
})
return nuxtError
} }

View File

@ -6,6 +6,7 @@ import type { NavigationFailure, RouteLocationNormalized, RouteLocationRaw, Rout
import type { AppConfig, RuntimeValue, UpperSnakeCase } from 'nuxt/schema' import type { AppConfig, RuntimeValue, UpperSnakeCase } from 'nuxt/schema'
import { defineNuxtConfig } from 'nuxt/config' import { defineNuxtConfig } from 'nuxt/config'
import { callWithNuxt, isVue3 } from '#app' import { callWithNuxt, isVue3 } from '#app'
import type { NuxtError } from '#app'
import type { NavigateToOptions } from '#app/composables/router' import type { NavigateToOptions } from '#app/composables/router'
import { NuxtLayout, NuxtLink, NuxtPage, WithTypes } from '#components' import { NuxtLayout, NuxtLink, NuxtPage, WithTypes } from '#components'
import { useRouter } from '#imports' import { useRouter } from '#imports'
@ -38,8 +39,11 @@ describe('API routes', () => {
expectTypeOf(useAsyncData('api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>() expectTypeOf(useAsyncData('api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>() expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>() expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<unknown> | null>>()
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | null>>()
// backwards compatibility
expectTypeOf(useAsyncData<any, Error>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>()
expectTypeOf(useAsyncData<any, NuxtError<string>>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>() expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()