Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(nuxt): use default type for initial value for composables #20968

Merged
merged 6 commits into from
May 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 37 additions & 30 deletions packages/nuxt/src/app/composables/asyncData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 9,12 @@ export type _Transform<Input = any, Output = any> = (input: Input) => Output
export type PickFrom<T, K extends Array<string>> = T extends Array<any>
? T
: T extends Record<string, any>
? keyof T extends K[number]
? T // Exact same keys as the target, skip Pick
: K[number] extends never
? T
: Pick<T, K[number]>
: T
? keyof T extends K[number]
? T // Exact same keys as the target, skip Pick
: K[number] extends never
? T
: Pick<T, K[number]>
: T

export type KeysOf<T> = Array<
T extends T // Include all keys of union types, not just common keys
Expand All @@ -32,11 32,12 @@ export interface AsyncDataOptions<
ResT,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> {
server?: boolean
lazy?: boolean
default?: () => DataT | Ref<DataT> | null
transform?: _Transform<ResT, DataT>,
default?: () => DefaultT | Ref<DefaultT>
transform?: _Transform<ResT, DataT>
pick?: PickKeys
watch?: MultiWatchSources
immediate?: boolean
Expand All @@ -53,7 54,7 @@ export interface AsyncDataExecuteOptions {
}

export interface _AsyncData<DataT, ErrorT> {
data: Ref<DataT | null>
data: Ref<DataT>
pending: Ref<boolean>
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
Expand All @@ -67,32 68,35 @@ export function useAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys>
): AsyncData<PickFrom<DataT, PickKeys>, DataE | null>
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
export function useAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys>
): AsyncData<PickFrom<DataT, PickKeys>, DataE | null>
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
export function useAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, 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<ResT>, AsyncDataOptions<ResT, DataT, PickKeys>]
let [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]

// Validate arguments
if (typeof key !== 'string') {
Expand All @@ -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
Expand All @@ -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<DataT, DataE>
const asyncData = { ...nuxt._asyncData[key] } as AsyncData<DataT | DefaultT, DataE>

asyncData.refresh = asyncData.execute = (opts = {}) => {
if (nuxt._asyncDataPromises[key]) {
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -248,30 252,33 @@ export function useLazyAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys>, DataE | null>
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
export function useLazyAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys>, DataE | null>
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
export function useLazyAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, DataE | null> {
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | 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<ResT>, AsyncDataOptions<ResT, DataT, PickKeys>]
const [key, handler, options] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
// @ts-expect-error we pass an extra argument to prevent a key being injected
return useAsyncData(key, handler, { ...options, lazy: true }, null)
}
Expand Down
33 changes: 19 additions & 14 deletions packages/nuxt/src/app/composables/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 19,10 @@ export interface UseFetchOptions<
ResT,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
R extends NitroFetchRequest = string & {},
M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys>, 'watch'>, ComputedFetchOptions<R, M> {
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'watch'>, ComputedFetchOptions<R, M> {
key?: string
$fetch?: typeof globalThis.$fetch
watch?: MultiWatchSources | false
Expand All @@ -34,22 35,24 @@ export function useFetch<
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>
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, ReqT, Method>
): AsyncData<PickFrom<DataT, PickKeys>, ErrorT | null>
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
export function useFetch<
ResT = void,
ErrorT = FetchError,
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>
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
request: Ref<ReqT> | 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]
Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand All @@ -127,27 130,29 @@ export function useLazyFetch<
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>
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, Method>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys>, ErrorT | null>
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, Method>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
export function useLazyFetch<
ResT = void,
ErrorT = FetchError,
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>
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
arg1?: string | Omit<UseFetchOptions<_ResT, DataT, PickKeys, Method>, 'lazy'>,
arg1?: string | Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, Method>, 'lazy'>,
arg2?: string
) {
const [opts, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]

return useFetch<ResT, ErrorT, ReqT, _ResT, PickKeys>(request, {
return useFetch<ResT, ErrorT, ReqT, DefaultT, _ResT, PickKeys>(request, {
...opts,
lazy: true
},
Expand Down
18 changes: 9 additions & 9 deletions test/fixtures/basic/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,13 275,13 @@ describe('composables', () => {
expectTypeOf(useCookie('test', { default: () => 500 })).toEqualTypeOf<Ref<number>>()
useCookie<number | null>('test').value = null

expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => ref(500) }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => 500 }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => ref(500) }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => 500 }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => ref(500) }).data).toEqualTypeOf<Ref<number>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => 500 }).data).toEqualTypeOf<Ref<number>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => ref(500) }).data).toEqualTypeOf<Ref<string | number>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => 500 }).data).toEqualTypeOf<Ref<string | number>>()

expectTypeOf(useFetch('/test', { default: () => ref(500) }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useFetch('/test', { default: () => 500 }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useFetch('/test', { default: () => ref(500) }).data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useFetch('/test', { default: () => 500 }).data).toEqualTypeOf<Ref<unknown>>()
})

it('infer request url string literal from server/api routes', () => {
Expand Down Expand Up @@ -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<Ref<{ bar: number } | null>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: { bar: 500 } }), { default: () => ({ bar: 500 }), transform: v => v.foo }).data).toEqualTypeOf<Ref<{ bar: number }>>()
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<Ref<string | null>>()
expectTypeOf(useLazyFetch('/api/hey', { default: () => 'bar', transform: v => v.foo }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/api/hey', { default: () => 1, transform: v => v.foo }).data).toEqualTypeOf<Ref<string | number>>()
expectTypeOf(useLazyFetch('/api/hey', { default: () => 'bar', transform: v => v.foo }).data).toEqualTypeOf<Ref<string>>()
})

it('uses types compatible between useRequestHeaders and useFetch', () => {
Expand Down