query icon indicating copy to clipboard operation
query copied to clipboard

narrowing typescript keeps `data` as possible undefined

Open Tbaile opened this issue 7 months ago • 6 comments

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

Tbaile avatar Jun 03 '25 16:06 Tbaile

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.

gs18004 avatar Jun 04 '25 10:06 gs18004

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.

Tbaile avatar Jun 04 '25 14:06 Tbaile

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.

DamianOsipiuk avatar Jun 04 '25 17:06 DamianOsipiuk

@Tbaile Thank you for your reply. I am working on this one too.

gs18004 avatar Jun 04 '25 17:06 gs18004

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.

Tbaile avatar Jun 05 '25 08:06 Tbaile

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.

DamianOsipiuk avatar Jun 05 '25 09:06 DamianOsipiuk