diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index b7c401aaa3..a5bd1e882d 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -9,12 +9,12 @@ export type _Transform = (input: Input) => Output export type PickFrom> = T extends Array ? T : T extends Record - ? keyof T extends K[number] - ? T // Exact same keys as the target, skip Pick - : K[number] extends never - ? T - : Pick - : T + ? keyof T extends K[number] + ? T // Exact same keys as the target, skip Pick + : K[number] extends never + ? T + : Pick + : T export type KeysOf = Array< T extends T // Include all keys of union types, not just common keys @@ -32,11 +32,12 @@ export interface AsyncDataOptions< ResT, DataT = ResT, PickKeys extends KeysOf = KeysOf, + DefaultT = null, > { server?: boolean lazy?: boolean - default?: () => DataT | Ref | null - transform?: _Transform, + default?: () => DefaultT | Ref + transform?: _Transform pick?: PickKeys watch?: MultiWatchSources immediate?: boolean @@ -53,7 +54,7 @@ export interface AsyncDataExecuteOptions { } export interface _AsyncData { - data: Ref + data: Ref pending: Ref refresh: (opts?: AsyncDataExecuteOptions) => Promise execute: (opts?: AsyncDataExecuteOptions) => Promise @@ -67,32 +68,35 @@ export function useAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( handler: (ctx?: NuxtApp) => Promise, - options?: AsyncDataOptions -): AsyncData, DataE | null> + options?: AsyncDataOptions +): AsyncData | DefaultT, DataE | null> export function useAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( key: string, handler: (ctx?: NuxtApp) => Promise, - options?: AsyncDataOptions -): AsyncData, DataE | null> + options?: AsyncDataOptions +): AsyncData | DefaultT, DataE | null> export function useAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > (...args: any[]): AsyncData, DataE | null> { const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined if (typeof args[0] !== 'string') { args.unshift(autoKey) } // eslint-disable-next-line prefer-const - let [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + let [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] // Validate arguments if (typeof key !== 'string') { @@ -104,7 +108,7 @@ export function useAsyncData< // Apply defaults options.server = options.server ?? true - options.default = options.default ?? getDefault + options.default = options.default ?? (getDefault as () => DefaultT) options.lazy = options.lazy ?? false options.immediate = options.immediate ?? true @@ -118,13 +122,13 @@ export function useAsyncData< // Create or use a shared asyncData entity if (!nuxt._asyncData[key]) { nuxt._asyncData[key] = { - data: ref(getCachedData() ?? options.default?.() ?? null), + data: ref(getCachedData() ?? options.default!()), pending: ref(!hasCachedData()), error: toRef(nuxt.payload._errors, key) } } // TODO: Else, somehow check for conflicting keys with different defaults or fetcher - const asyncData = { ...nuxt._asyncData[key] } as AsyncData + const asyncData = { ...nuxt._asyncData[key] } as AsyncData asyncData.refresh = asyncData.execute = (opts = {}) => { if (nuxt._asyncDataPromises[key]) { @@ -167,7 +171,7 @@ export function useAsyncData< if ((promise as any).cancelled) { return nuxt._asyncDataPromises[key] } asyncData.error.value = error - asyncData.data.value = unref(options.default?.() ?? null) + asyncData.data.value = unref(options.default!()) }) .finally(() => { if ((promise as any).cancelled) { return } @@ -248,30 +252,33 @@ export function useLazyAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( handler: (ctx?: NuxtApp) => Promise, - options?: Omit, 'lazy'> -): AsyncData, DataE | null> + options?: Omit, 'lazy'> +): AsyncData | DefaultT, DataE | null> export function useLazyAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( key: string, handler: (ctx?: NuxtApp) => Promise, - options?: Omit, 'lazy'> -): AsyncData, DataE | null> + options?: Omit, 'lazy'> +): AsyncData | DefaultT, DataE | null> export function useLazyAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf -> (...args: any[]): AsyncData, DataE | null> { + PickKeys extends KeysOf = KeysOf, + DefaultT = null, +> (...args: any[]): AsyncData | DefaultT, DataE | null> { const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined if (typeof args[0] !== 'string') { args.unshift(autoKey) } - const [key, handler, options] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + const [key, handler, options] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] // @ts-expect-error we pass an extra argument to prevent a key being injected return useAsyncData(key, handler, { ...options, lazy: true }, null) } diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index c06271cb43..ba77fe8c24 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -19,9 +19,10 @@ export interface UseFetchOptions< ResT, DataT = ResT, PickKeys extends KeysOf = KeysOf, + DefaultT = null, R extends NitroFetchRequest = string & {}, M extends AvailableRouterMethod = AvailableRouterMethod -> extends Omit, 'watch'>, ComputedFetchOptions { +> extends Omit, 'watch'>, ComputedFetchOptions { key?: string $fetch?: typeof globalThis.$fetch watch?: MultiWatchSources | false @@ -34,11 +35,12 @@ export function useFetch< Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, _ResT = ResT extends void ? FetchResult : ResT, DataT = _ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( request: Ref | ReqT | (() => ReqT), - opts?: UseFetchOptions<_ResT, DataT, PickKeys, ReqT, Method> -): AsyncData, ErrorT | null> + opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method> +): AsyncData | DefaultT, ErrorT | null> export function useFetch< ResT = void, ErrorT = FetchError, @@ -46,10 +48,11 @@ export function useFetch< Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, _ResT = ResT extends void ? FetchResult : ResT, DataT = _ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( request: Ref | ReqT | (() => ReqT), - arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, ReqT, Method>, + arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, arg2?: string ) { const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] @@ -91,7 +94,7 @@ export function useFetch< cache: typeof opts.cache === 'boolean' ? undefined : opts.cache }) - const _asyncDataOptions: AsyncDataOptions<_ResT, DataT, PickKeys> = { + const _asyncDataOptions: AsyncDataOptions<_ResT, DataT, PickKeys, DefaultT> = { server, lazy, default: defaultFn, @@ -103,7 +106,7 @@ export function useFetch< let controller: AbortController - const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys>(key, () => { + const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, () => { controller?.abort?.() controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController @@ -127,11 +130,12 @@ export function useLazyFetch< Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, _ResT = ResT extends void ? FetchResult : ResT, DataT = _ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( request: Ref | ReqT | (() => ReqT), - opts?: Omit, 'lazy'> -): AsyncData, ErrorT | null> + opts?: Omit, 'lazy'> +): AsyncData | DefaultT, ErrorT | null> export function useLazyFetch< ResT = void, ErrorT = FetchError, @@ -139,15 +143,16 @@ export function useLazyFetch< Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, _ResT = ResT extends void ? FetchResult : ResT, DataT = _ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( request: Ref | ReqT | (() => ReqT), - arg1?: string | Omit, 'lazy'>, + arg1?: string | Omit, 'lazy'>, arg2?: string ) { const [opts, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] - return useFetch(request, { + return useFetch(request, { ...opts, lazy: true }, diff --git a/test/fixtures/basic/types.ts b/test/fixtures/basic/types.ts index 1b61186a60..cd3b1386dd 100644 --- a/test/fixtures/basic/types.ts +++ b/test/fixtures/basic/types.ts @@ -275,13 +275,13 @@ describe('composables', () => { expectTypeOf(useCookie('test', { default: () => 500 })).toEqualTypeOf>() useCookie('test').value = null - expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => ref(500) }).data).toEqualTypeOf>() - expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => 500 }).data).toEqualTypeOf>() - expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => ref(500) }).data).toEqualTypeOf>() - expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => 500 }).data).toEqualTypeOf>() + expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => ref(500) }).data).toEqualTypeOf>() + expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => 500 }).data).toEqualTypeOf>() + expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => ref(500) }).data).toEqualTypeOf>() + expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => 500 }).data).toEqualTypeOf>() - expectTypeOf(useFetch('/test', { default: () => ref(500) }).data).toEqualTypeOf>() - expectTypeOf(useFetch('/test', { default: () => 500 }).data).toEqualTypeOf>() + expectTypeOf(useFetch('/test', { default: () => ref(500) }).data).toEqualTypeOf>() + expectTypeOf(useFetch('/test', { default: () => 500 }).data).toEqualTypeOf>() }) it('infer request url string literal from server/api routes', () => { @@ -313,11 +313,11 @@ describe('composables', () => { .toEqualTypeOf(useLazyAsyncData(() => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo })) // Default values: #14437 - expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: { bar: 500 } }), { default: () => ({ bar: 500 }), transform: v => v.foo }).data).toEqualTypeOf>() + expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: { bar: 500 } }), { default: () => ({ bar: 500 }), transform: v => v.foo }).data).toEqualTypeOf>() expectTypeOf(useLazyAsyncData('test', () => Promise.resolve({ foo: { bar: 500 } }), { default: () => ({ bar: 500 }), transform: v => v.foo })) .toEqualTypeOf(useLazyAsyncData(() => Promise.resolve({ foo: { bar: 500 } }), { default: () => ({ bar: 500 }), transform: v => v.foo })) - expectTypeOf(useFetch('/api/hey', { default: () => 'bar', transform: v => v.foo }).data).toEqualTypeOf>() - expectTypeOf(useLazyFetch('/api/hey', { default: () => 'bar', transform: v => v.foo }).data).toEqualTypeOf>() + expectTypeOf(useFetch('/api/hey', { default: () => 1, transform: v => v.foo }).data).toEqualTypeOf>() + expectTypeOf(useLazyFetch('/api/hey', { default: () => 'bar', transform: v => v.foo }).data).toEqualTypeOf>() }) it('uses types compatible between useRequestHeaders and useFetch', () => {