React Query 和 TypeScript 泛型类型缩小使用启用选项的类型安全乐观更新 useInfiniteQuery 输入默认查询函数

2025-06-10

React Query 和 TypeScript

泛型

类型缩小

使用启用选项进行类型安全

乐观更新

使用无限查询

输入默认查询函数

TypeScript就是🔥——这似乎已经成为了前端社区的共识。许多开发者希望库要么用 TypeScript 编写,要么至少提供良好的类型定义。对我来说,如果一个库是用 TypeScript 编写的,那么类型定义就是最好的文档。它永远不会错,因为它直接反映了实现。我经常在阅读 API 文档之前先查看类型定义。

React Query 最初是用 JavaScript (v1) 编写的,后来在 v2 中被重写为 TypeScript。这意味着现在它对 TypeScript 用户提供了非常好的支持。

然而,由于 React Query 的动态性和不拘泥于形式,使用 TypeScript 时会遇到一些“陷阱”。让我们逐一介绍这些陷阱,以提升你的使用体验。

泛型

React Query 大量使用泛型。这是必要的,因为该库实际上不会为你获取数据,并且它无法知道你的 API 返回的数据是什么类型。

官方文档中的 TypeScript 部分不是很详细,它告诉我们在调用useQuery时要明确指定它所期望的泛型:



function useGroups() {
    return useQuery<Group[], Error>('groups', fetchGroups)
}


Enter fullscreen mode Exit fullscreen mode

随着时间的推移,React Query 为useQuery hook添加了更多泛型(现在有四个),主要是因为添加了更多功能。上面的代码可以正常工作,并且它将确保我们自定义 hook 的data属性的类型正确,Group[] | undefined并且我们的错误类型为Error | undefined。但是,对于更高级的用例,尤其是在需要另外两个泛型的情况下,它就无法正常工作了。

四种泛型

这是useQuery钩子的当前定义



export function useQuery<
    TQueryFnData = unknown,
    TError = unknown,
    TData = TQueryFnData,
    TQueryKey extends QueryKey = QueryKey
>


Enter fullscreen mode Exit fullscreen mode

有很多事情正在发生,所以让我们尝试将其分解:

  • TQueryFnData: queryFn返回的类型。在上面的例子中,它是Group[]
  • TError:示例中queryFn所期望的错误类型Error
  • TData:我们的数据属性最终将具有的类型。仅在使用select选项时才有意义,因为此时数据属性可能与queryFn返回的值不同。否则,它将默认为queryFn返回的值。
  • TQueryKey:我们的 QueryKey 的类型,仅当您使用传递给queryFn的 QueryKey 时才相关

您还可以看到,所有这些泛型都有默认值,这意味着如果您不提供它们,TypeScript 将回退到这些类型。这与 JavaScript 中的默认参数几乎相同:



function multiply(a, b = 2) {
    return a * b
}

multiply(10) // ✅ 20
multiply(10, 3) // ✅ 30


Enter fullscreen mode Exit fullscreen mode

类型推断

如果 TypeScript 能够自行推断(或推断)某个对象应该是什么类型,那么它会发挥最佳效果。这不仅使代码更易于编写(因为你不必输入所有类型😅),而且还使代码更易于阅读在很多情况下,它可以使代码看起来与 JavaScript 完全一样。类型推断的一些简单示例如下:



const num = Math.random() + 5 // ✅ `number`

// 🚀 both greeting and the result of greet will be string
function greet(greeting = 'ciao') {
    return `${greeting}, ${getName()}`
}


Enter fullscreen mode Exit fullscreen mode

说到泛型,通常也可以从其用法推断出来,这非常棒。你也可以手动提供它们,但很多情况下,你不需要这样做。



function identity<T>(value: T): T {
    return value
}

// 🚨 no need to provide the generic
let result = identity<number>(23)

// ⚠️ or to annotate the result
let result: number = identity(23)

// 😎 infers correctly to `string`
let result = identity('react-query')


Enter fullscreen mode Exit fullscreen mode

部分类型参数推断

…在 TypeScript 中尚不存在(请参阅此未解决的问题)。这基本上意味着,如果你提供了一个泛型,就必须提供所有泛型。但是由于 React Query 为泛型提供了默认值,我们可能不会立即注意到它们会被使用。由此产生的错误消息可能非常隐晦。让我们看一个实际上适得其反的例子:



function useGroupCount() {
    return useQuery<Group[], Error>('groups', fetchGroups, {
        select: (groups) => groups.length,
        // 🚨 Type '(groups: Group[]) => number' is not assignable to type '(data: Group[]) => Group[]'.
        // Type 'number' is not assignable to type 'Group[]'.ts(2322)
    })
}


Enter fullscreen mode Exit fullscreen mode

因为我们没有提供第三个泛型,所以默认值也会生效,也就是Group[],但我们numberselect函数返回了。一种解决方法是直接添加第三个泛型:



function useGroupCount() {
    // ✅ fixed it
    return useQuery<Group[], Error, number>('groups', fetchGroups, {
        select: (groups) => groups.length,
    })
}


Enter fullscreen mode Exit fullscreen mode

只要我们没有部分类型参数推断,我们就必须利用我们所得到的东西。

那么还有什么其他选择呢?

推断所有的事情

首先,我们完全传入任何泛型,让 TypeScript 自行处理。为了实现这一点,我们需要queryFn有一个合适的返回类型。当然,如果你内联该函数时没有明确指定返回类型,那么返回类型是any 的——因为axiosfetch会提供这种返回类型:



function useGroups() {
    // 🚨 data will be `any` here
    return useQuery('groups', () => axios.get('groups').then((response) => respone.data))
}


Enter fullscreen mode Exit fullscreen mode

如果您(像我一样)喜欢将您的 api 层与查询分开,那么您无论如何都需要添加类型定义以避免隐式 any,以便 React Query 可以推断其余部分:



function fetchGroups(): Promise<Group[]> {
    return axios.get('groups').then((response) => response.data)
}

// ✅ data will be `Group[] | undefined` here
function useGroups() {
    return useQuery('groups', fetchGroups)
}

// ✅ data will be `number | undefined` here
function useGroupCount() {
    return useQuery('groups', fetchGroups, {
        select: (groups) => groups.length,
    })
}


Enter fullscreen mode Exit fullscreen mode

这种方法的优点是:

  • 不再需要手动指定泛型
  • 适用于需要第三(选择)和第四(QueryKey)通用的情况
  • 如果添加更多泛型,将继续工作
  • 代码不那么令人困惑/看起来更像 JavaScript

错误怎么办?

你可能会问,那 error 怎么办?默认情况下,如果不使用泛型,error 会被推断为unknown。这听起来像个 bug,为什么它不是Error呢?但这实际上是故意的,因为在 JavaScript 中,你可以抛出任何错误——它不必是 类型Error



throw 5
throw undefined
throw Symbol('foo')


Enter fullscreen mode Exit fullscreen mode

由于 React Query 不负责返回 Promise 的函数,因此它也无法知道它可能产生的错误类型。因此,unknown是正确的。一旦 TypeScript 允许在调用包含多个泛型的函数时跳过某些泛型(有关更多信息,请参阅此问题),我们就可以更好地处理这个问题。但就目前而言,如果我们需要处理错误并且不想依赖传递泛型,我们可以使用 intnanceof 检查来缩小类型范围:



const groups = useGroups()

if (groups.error) {
    // 🚨 this doesn't work because: Object is of type 'unknown'.ts(2571)
    return <div>An error occurred: {groups.error.message}</div>
}

// ✅ the instanceOf check narrows to type `Error`
if (groups.error instanceof Error) {
    return <div>An error occurred: {groups.error.message}</div>
}


Enter fullscreen mode Exit fullscreen mode

既然我们无论如何都需要进行某种检查来判断是否存在错误,那么使用 instanceof 检查似乎是一个不错的选择,而且它还能确保我们的错误在运行时确实具有属性 message。这也符合 TypeScript 4.4 版本的计划,他们将在该版本中引入一个新的编译器标志useUnknownInCatchVariables,其中捕获变量将为unknown而不是any(参见此处)。

类型缩小

在使用 React Query 时,我很少使用解构。首先,像dataerror这样的名字非常通用(我故意这么用),所以你很可能无论如何都会重命名它们。保留整个对象可以保留数据本身或错误来源的上下文。这还能帮助 TypeScript 在使用 status 字段或某个 status 布尔值时缩小类型范围,而使用解构则无法做到这一点:



const { data, isSuccess } = useGroups()
if (isSuccess) {
    // 🚨 data will still be `Group[] | undefined` here
}

const groupsQuery = useGroups()
if (groupsQuery.isSuccess) {
    // ✅ groupsQuery.data will now be `Group[]`
}


Enter fullscreen mode Exit fullscreen mode

这与 React Query 无关,只是 TypeScript 的工作原理。@danvdk对这种行为有很好的解释。

使用启用选项进行类型安全

我从一开始就表达了对启用选项的喜爱,但如果您想将它用于依赖查询并在某些参数尚未定义的情况下禁用查询,那么在类型级别上可能会有点棘手:



function fetchGroup(id: number): Promise<Group> {
    return axios.get(`group/${id}`).then((response) => response.data)
}

function useGroup(id: number | undefined) {
    return useQuery(['group', id], () => fetchGroup(id), { enabled: Boolean(id) })
    // 🚨 Argument of type 'number | undefined' is not assignable to parameter of type 'number'.
    //  Type 'undefined' is not assignable to type 'number'.ts(2345)
}


Enter fullscreen mode Exit fullscreen mode

从技术上讲,TypeScript 是正确的,id可能为undefinedenabled选项不会执行任何类型缩小操作。此外,还有一些方法可以绕过enabled选项,例如通过调用useQuery返回的refetch方法。在这种情况下,id可能确实是undefined

如果你不喜欢非空断言运算符,我发现最好的方法是接受id可以为undefined ,并拒绝queryFn中的 Promise 。虽然有点重复,但也很明确且安全:



function fetchGroup(id: number | undefined): Promise<Group> {
    // ✅ check id at runtime because it can be `undefined`
    return typeof id === 'undefined'
        ? Promise.reject(new Error('Invalid id'))
        : axios.get(`group/${id}`).then((response) => response.data)
}

function useGroup(id: number | undefined) {
    return useQuery(['group', id], () => fetchGroup(id), { enabled: Boolean(id) })
}


Enter fullscreen mode Exit fullscreen mode

乐观更新

在 TypeScript 中正确获取乐观更新并非易事,因此我们决定将其作为一个综合示例添加到文档中。

重点是:为了获得最佳的类型推断,你必须明确指定传递给onMutate 的变量参数的类型。我不太明白为什么会这样,但这似乎与泛型推断有关。更多信息,请查看此评论。

使用无限查询

在大多数情况下,输入useInfiniteQuery与输入useQuery没什么区别。一个值得注意的问题是,传递给queryFn 的pageParam值的类型是any。库中肯定可以改进,但只要它是any,最好还是明确地注释它:



type GroupResponse = { next?: number, groups: Group[] }
const queryInfo = useInfiniteQuery(
    'groups',
    // ⚠️ explicitly type pageParam to override `any`
    ({ pageParam = 0 }: { pageParam: GroupResponse['next']) => fetchGroups(groups, pageParam),
    {
        getNextPageParam: (lastGroup) => lastGroup.next,
    }
)


Enter fullscreen mode Exit fullscreen mode

如果fetchGroups返回GroupResponse,则lastGroup将很好地推断其类型,并且我们可以使用相同的类型来注释pageParam

输入默认查询函数

我个人不使用defaultQueryFn,但我知道很多人会用。这是一种巧妙的方法,可以利用传递的queryKey直接构建请求 URL。如果在创建queryClient时内联该函数,传递的QueryFunctionContext的类型也会被推断出来。内联后,TypeScript 的性能会好得多 :)



const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            queryFn: async ({ queryKey: [url] }) => {
                const { data } = await axios.get(`${baseUrl}/${url}`)
                return data
            },
        },
    },
})


Enter fullscreen mode Exit fullscreen mode

这确实有效,但是url会被推断为unknown类型,因为整个 queryKey 是一个未知的 Array。在创建 queryClient 时,调用useQuery时 queryKeys 的构造方式完全无法保证,所以 React Query 能做的事情非常有限。这就是这个高度动态特性的本质。但这并非坏事,因为这意味着你现在必须采取预防措施,并通过运行时检查来缩小类型范围才能使用它,例如:



const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            queryFn: async ({ queryKey: [url] }) => {
                // ✅ narrow the type of url to string so that we can work with it
                if (typeof url === 'string') {
                    const { data } = await axios.get(`${baseUrl}/${url.toLowerCase()}`)
                    return data
                }
                throw new Error('Invalid QueryKey')
            },
        },
    },
})


Enter fullscreen mode Exit fullscreen mode

我认为这很好地解释了为什么unknownany更优秀(且未被充分利用)的类型。它最近成了我最喜欢的类型——不过这又是另一篇博文的主题了😊。


今天就到这里。如果你有任何问题,欢迎在推特上联系我
,或者在下方留言⬇️

鏂囩珷鏉ユ簮锛�https://dev.to/tkdodo/react-query-and-typescript-34ai
PREV
关于 useState 你需要知道的事情 1:函数式更新器 2:惰性初始化器 3:更新救援 4:便利性过载 5:实现细节 结论
NEXT
不要过度使用State 什么是State?一个例子 不同步 无用状态