swr icon indicating copy to clipboard operation
swr copied to clipboard

Type-narrowing doesn't work

Open lcswillems opened this issue 1 year ago • 14 comments

Bug report

Description / Observed Behavior

const { data, error, isLoading } = useSWR<number>('/api/user/123', fetcher);

if (!isLoading && !error) {
  data // number | undefined
}

Expected Behavior

It should be of type number.

lcswillems avatar Dec 17 '24 09:12 lcswillems

I don't think useSWR returns isSuccess. Are you mistyping something @lcswillems ?

Ram4GB avatar Dec 17 '24 13:12 Ram4GB

@Ram4GB My bad. Fixed the example!

lcswillems avatar Dec 17 '24 16:12 lcswillems

I assume you request the user who is not found in your database or has been deleted by other actions. In this case, the API must throw the error with the status 400, so you won't access your data, and it has to return undefined. @lcswillems

Ram4GB avatar Dec 18 '24 10:12 Ram4GB

Not sure to understand. I'm just saying if "isLoading" is false, then "data" should become of type "number".

It is a basic functionality of TanStack query: https://tanstack.com/query/latest/docs/framework/react/typescript#type-narrowing

lcswillems avatar Dec 18 '24 11:12 lcswillems

But if fetching fails/errors, there won't be data, and it won't be loading either.

icyJoseph avatar Dec 18 '24 11:12 icyJoseph

@icyJoseph Updated my example. Makes sense? Basically I'm asking for the same thing than TanStack Query.

lcswillems avatar Dec 18 '24 12:12 lcswillems

Yes @lcswillems, your example is not clear to me. In your case, I believe we should get the type number instead of number | undefined.

Ram4GB avatar Dec 19 '24 03:12 Ram4GB

Yes we should get the type number, it is what I say in the "expected behavior". Currently, the type is number | undefined (Observed behavior).

lcswillems avatar Dec 19 '24 08:12 lcswillems

@lcswillems I get your point. Basically there are three ways to achieve your goal 1. const { data, error, isLoading } = useSWR<number>('/api/user/123', fetcher, { suspense: true }); 2. const { data, error, isLoading } = useSWR<number>('/api/user/123', fetcher, { fallbackData: 0 }); 3.

const { data, error, isLoading } = useSWR<number>('/api/user/123', fetcher);

if (!isLoading && !error && data !== undefined) {
  data // number
}

samuel871211 avatar Jan 23 '25 12:01 samuel871211

Thank you for sharing!

  1. Uses suspense. Don't want to go in that direction.

  2. data would then be always a number. I still want it to be potentially undefined.

  3. Yeah but the whole point of this issue is to say I should not have to add data !== undefined.

The 4th and best option would be to improve the typing of the library.

lcswillems avatar Jan 23 '25 13:01 lcswillems

@lcswillems Improve the typing of the library seems to be the best solution, but there is a bit difference between your example and TanStack query.

Your example

const { data, error, isLoading } = useSWR<number>('/api/user/123', fetcher);

if (!isLoading && !error) {
  data // number | undefined
}

TanStack query

const { data, isSuccess } = useQuery({
  queryKey: ['test'],
  queryFn: () => Promise.resolve(5),
})

if (isSuccess) {
  data
  //  ^? const data: number
}

TanStack query creates a new field isSuccess to determine whether type of the data is Data | undefined or Data

samuel871211 avatar Jan 26 '25 02:01 samuel871211

@samuel871211 In TanStack query, this also narrows down:

const { data, isLoading, isError } = useQuery({ ... });

if (!isLoading && !isError) {
  data
  //  ^? const data: number
}

And I'm wondering what prevents SWR to narrow down here:

const { data, error, isLoading } = useSWR<number>('/api/user/123', fetcher);

if (!isLoading && !error) {
  data // number
}

?

lcswillems avatar Jan 26 '25 08:01 lcswillems

@lcswillems

  1. I'm using "@tanstack/react-query": "^5.64.2"

Your code example doesn't work for me.

const { data, isLoading, isError } = useQuery({ ... });

if (!isLoading && !isError) {
  data
  //  ^? const data: number
}
Image
  1. I believe SWR currently doesn't have this feature, so I have made a feature PR.

Original Codebase before PR

export interface SWRResponse<Data = any, Error = any, Config = any> {
  /**
   * The returned data of the fetcher function.
   */
  data: BlockingData<Data, Config> extends true ? Data : Data | undefined
  ......
}

export type BlockingData<
  Data = any,
  Options = SWROptions<Data>
> = Options extends undefined
  ? false
  : Options extends { suspense: true }
  ? true
  : Options extends { fallbackData: Data | Promise<Data> }
  ? true
  : false

The type of data is Data only when the user provides options.suspense or options.fallbackData.

samuel871211 avatar Jan 26 '25 11:01 samuel871211

@samuel871211 Sorry it is "isPending" that needs to be used:

Image

https://stackblitz.com/edit/github-j5aexizp?file=src%2Findex.tsx

lcswillems avatar Jan 26 '25 12:01 lcswillems