narrowing typescript keeps `data` as possible undefined
Describe the bug
Hello, I'm using query to fetch some data coming from the server using Vue, the code looks like this:
const { error, data, status } = useQuery({
queryKey: ['units', page],
queryFn: () => axios.get<PaginatedResponse<Unit>>('/api/units?page=' + page.value),
select: (response) => response.data,
placeholderData: keepPreviousData,
})
per docs, if status == success or isSuccess is true, we should have data defined with type from the calling queryFn. However it appears by @tanstack/query-core type definition that this is not the case:
interface SuccessAction<TData> {
data: TData | undefined
type: 'success'
dataUpdatedAt?: number
manual?: boolean
}
Is this wanted due to uncertainty of the data being returned? If that so, shouldn't this be responsibility of the type definition of the called queryFn?
Your minimal, reproducible example
https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAOQACMAhgHaoMDGA1gPQBuOAtAEcc+KgFgAUBImsIzeEgAm9BgBo4wVAGVkrVulSoicALwoM2XHgAUCCXHtwhlgGKMAXHCsBKEwD44AArkIBroAHRQ+hAANtzoVgCsXip2Dk74ANLoeB4A2lQw+jBUALoShF5S4sAknhrauvqoPrbiDnBKDBKcnPYAegD85RJAA
Steps to reproduce
Simply check in typescript playground, data is of type Ref<number, number> | Ref<undefined, undefined> even when resolved and inside a if check for success
Expected behavior
Per doc, this shouldn't be allowed
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
Issue is platform agnostic
Tanstack Query adapter
vue-query
TanStack Query version
v5.76.0
TypeScript version
5.8.3
Additional context
No response
I don't know vue, so it might be wrong but if you look at the test code in the source,
it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => {
const { data, isSuccess } = reactive(
useQuery({
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: () => undefined as { wow: boolean } | undefined,
}),
)
if (isSuccess) {
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
}
})
the type is being narrowed.
The difference is that the test code used reactive.
True, it seems that wrapping in reactive solves the issue inside the script section, but not entirely when using data inside a template.
Type narrowing is the only section in the whole doc that refers to reactive wrap around the useQuery, probably a leftover from V4? Changelog mentions that indeed are being returned refs now, which are reactive: https://tanstack.com/query/latest/docs/framework/vue/guides/migrating-to-v5#vue-query-breaking-changes.
Here's a typescript build failure extract on the project I'm working on:
resources/js/views/Units.vue:43:35 - error TS18048: '__VLS_ctx.data' is possibly 'undefined'.
43 v-if="data.meta.total > data.meta.per_page"
~~~~
resources/js/views/Units.vue:43:53 - error TS18048: '__VLS_ctx.data' is possibly 'undefined'.
43 v-if="data.meta.total > data.meta.per_page"
~~~~
resources/js/views/Units.vue:44:36 - error TS18048: '__VLS_ctx.data' is possibly 'undefined'.
44 :rows="data.meta.per_page"
~~~~
resources/js/views/Units.vue:45:45 - error TS18048: '__VLS_ctx.data' is possibly 'undefined'.
45 :total-records="data.meta.total"
~~~~
Found 4 errors.
This happens even with a if check before that:
<template v-if="isSuccess">
<Paginator
v-if="data.meta.total > data.meta.per_page"
:rows="data.meta.per_page"
:total-records="data.meta.total"
@page="(event) => (page = event.page)"
/>
</template>
I'm pretty sure union types and type narrowing work on Vue templates since I've used them before. I can try to provide a working example in something other than typescript playground.
Yes, there is a problem with type narrowing in templates due to usage of Ref wrapper around original types from core.
I believe I tried to fix it before without rewriting types coming from the core with no luck so far.
@Tbaile Thank you for your reply. I am working on this one too.
Is there any issue with editing the core types? We'll be editing a narrow type anyway that forces data to have the TData value set. Will try later to custom build the package editing the SuccessAction type and will let you know if anything changes.
I believe the issue is not with SuccessAction but with type union of possible results
https://github.com/TanStack/query/blob/37eda0d7d9884fef95a6c349b8be15c6846fc07d/packages/query-core/src/types.ts#L895-L900
I'ts creating a union on plain values which we then wrap with MaybeRef that makes typescript not aware how to narrow it down.
I think that the proper fix would involve reimplementing this union and all types that its using to include Ref in their signature for proper type narrowing.