首页 / TanStack 生态入门教程 / TypeScript 与类型安全基础

TanStack 生态入门教程

TypeScript 与类型安全基础

本教程共 38 篇 · 第 3 篇 · 更新于 2026-07-27 · 约 9 分钟阅读

TanStackTanStack 生态入门教程TypeScript类型安全类型推导queryOptions泛型Register

3. TypeScript 与类型安全基础

本节目标:搞懂 TanStack 的类型安全怎么用、怎么推导、遇到类型问题怎么解决,学完你能写出类型严密的查询和路由代码,少踩 any 的坑。

3.1 TanStack 的类型安全哲学

先说个原则:TanStack 的目标是让你尽量少写类型

听起来矛盾?类型安全不是应该多写类型吗?恰恰相反。好的类型安全是「自动推导」,你给运行时数据,TS 自己推类型,不用你手标。

TanStack Form 官方原话:写正确的 TanStack 代码时,除了对运行时值的类型标注,你不应该能区分这是 JavaScript 还是 TypeScript 用法。

Query 也遵循这个原则。queryFn 返回 Promise<number>data 就自动是 number | undefined,一个泛型都不用传。

3.2 useQuery 的自动推导

看个最简单的例子:

const { data } = useQuery({
  queryKey: ['test'],
  queryFn: () => Promise.resolve(5),
})
//      ^? const data: number | undefined

queryFn 返回 Promise<number>data 自动推导成 number | undefinedundefined 是因为查询还没成功时 data 是没有的。

加了 select 也能推导。select 会改变 data 的类型:

const { data } = useQuery({
  queryKey: ['test'],
  queryFn: () => Promise.resolve(5),
  select: (data) => data.toString(),
})
//      ^? const data: string | undefined

select 接收 number,返回 stringdata 就变成 string | undefined

Tip

自动推导要工作得好,前提是你的 queryFn 有明确的返回类型。很多数据请求库(比如 axios)默认返回 any,记得把请求函数单独抽出来标好类型:

// 标好返回类型
const fetchGroups = (): Promise<Group[]> =>
  axios.get('/groups').then((response) => response.data)

const { data } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
//      ^? const data: Group[] | undefined

3.3 状态收窄(Type Narrowing)

Query 的返回结果是个可辨识联合类型(discriminated union),靠 status 字段区分。这意味着你可以用状态判断来收窄 data 的类型:

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

if (isSuccess) {
  data
  //  ^? const data: number  -- 这里 data 不是 undefined 了
}

判了 isSuccess 之后,data 自动收窄成 number,不用再 data! 非空断言。这种写法既安全又省心。

3.4 错误类型怎么处理

v5 有个重要变更:error 的默认类型从 unknown 变成了 Error

为什么这么改?因为虽然 JS 里 throw 啥都行(所以 unknown 最正确),但实际项目里大家基本都 throw 一个 Error 或其子类。默认 Error 能让大多数场景少写类型标注。

const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
//      ^? const error: Error | null

自定义错误类型

如果你想抛自定义错误,比如 axios 的 AxiosError,有两个办法。

办法一:用类型守卫收窄:

import axios from 'axios'

const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
//      ^? const error: Error | null

if (axios.isAxiosError(error)) {
  error
  // ^? const error: AxiosError  -- 收窄成具体类型
}

办法二:传泛型(不推荐,会让其他泛型推导失效):

// 这种写法会让所有泛型都得手写,不推荐
const { error } = useQuery<Group[], string>({
  queryKey: ['groups'],
  queryFn: fetchGroups,
})
//      ^? const error: string | null
Warning

官方不推荐抛非 Error 的东西。如果一定要抛,考虑继承 Error 造个子类,而不是直接 throw 字符串或对象。

全局注册错误类型

如果你整个项目都用同一种自定义错误,v5 支持通过 Register 接口全局注册:

import '@tanstack/react-query'

declare module '@tanstack/react-query' {
  interface Register {
    // 用 unknown 强制调用方手动收窄
    defaultError: unknown
  }
}

const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
//      ^? const error: unknown | null

设成 unknown 是最严格的,逼着每个调用方显式判断错误类型,比较适合对类型要求极致的项目。

3.5 queryOptions:类型穿透利器

前面提过 queryOptions,这里展开讲讲为什么它重要。

问题场景:你想把查询配置抽成函数复用,但抽出来之后类型丢了。

// 抽出来的函数,类型推导没了
function groupOptions() {
  return {
    queryKey: ['groups'],
    queryFn: fetchGroups,
    staleTime: 5 * 1000,
  }
}

// 这里 data 变成 unknown 了
const data = queryClient.getQueryData(groupOptions().queryKey)
//     ^? unknown

queryOptions 包一层,类型就回来了:

import { queryOptions } from '@tanstack/react-query'

function groupOptions() {
  return queryOptions({
    queryKey: ['groups'],
    queryFn: fetchGroups,
    staleTime: 5 * 1000,
  })
}

const data = queryClient.getQueryData(groupOptions().queryKey)
//     ^? Group[] | undefined  -- 类型回来了

运行时 queryOptions 就是原样返回你传的东西,没任何开销。但在类型层面,它把 queryKeyqueryFn 的关系记住了,让 getQueryData 知道这个 key 对应什么数据。

mutationOptions 是 mutation 版的同类工具,用法一样。

3.6 Register:模块级类型注册

TanStack 有个贯穿各库的 Register 机制。通过「声明合并」往模块接口上加类型,让全局导出的 API 都能用到你的类型。

Query 里能注册这些东西:

import '@tanstack/react-query'

declare module '@tanstack/react-query' {
  interface Register {
    defaultError: unknown          // 全局错误类型
    queryMeta: MyMeta              // 查询的 meta 字段类型
    mutationMeta: MyMeta           // 变更的 meta 字段类型
    queryKey: MyQueryKey           // 查询键类型
    mutationKey: MyQueryKey        // 变更键类型
  }
}

注册 queryKey 类型能给查询键加结构约束,让它符合你应用的层级规范:

type QueryKey = ['dashboard' | 'marketing', ...ReadonlyArray<unknown>]

declare module '@tanstack/react-query' {
  interface Register {
    queryKey: QueryKey
  }
}

这样所有 queryKey 第一项只能是 'dashboard''marketing',写错就报错。

3.7 路由类型注册

Router 把 Register 机制用得更深。你的整个路由树类型都得注册上去:

const router = createRouter({
  routeTree,
  // ...
})

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

注册之后,这些全局导出的 API 全都有类型:

  • <Link to="/posts" />to 只能是合法路由路径
  • useParams() — 拿到的参数类型和路由定义一致
  • useSearch() — search params 类型受约束
  • useNavigate() — 导航目标有类型校验

写错路径、传错参数、search params 类型不对,统统编译报错。这是 React Router 给不了的体验。

路由上下文的类型

Router 还有个「路由上下文(Router Context)」机制,能往路由树里注入依赖(比如 QueryClient):

const rootRoute = createRootRouteWithContext<{
  queryClient: QueryClient
}>()({
  component: App,
})

const routeTree = rootRoute.addChildren([
  // 所有子路由都能拿到 queryClient
])

const router = createRouter({
  routeTree,
  context: {
    queryClient,  // 这里必须传,类型会校验
  },
})

createRootRouteWithContext 会把上下文类型贯穿整个路由树,每一层路由都能拿到注入的依赖,类型不会丢。

3.8 类型安全的查询禁用:skipToken

最后说个小但实用的特性。有时你想根据条件禁用查询,通常用 enabled: false。但 TypeScript 用户有个更类型安全的写法:skipToken

import { skipToken, useQuery } from '@tanstack/react-query'

function Todos() {
  const [filter, setFilter] = React.useState<string | undefined>()

  const { data } = useQuery({
    queryKey: ['todos', filter],
    // filter 为空时传 skipToken,类型仍然安全
    queryFn: filter ? () => fetchTodos(filter) : skipToken,
  })
}

skipToken 是个特殊的标记值,传给它等于禁用查询,但 queryFn 的类型签名不会被破坏。

Warning

用了 skipToken 之后,useQuery 返回的 refetch 就不能用了,调用会报 Missing queryFn 错误。需要手动触发查询的场景,还是用 enabled: false 配合 refetch

3.9 类型性能小贴士

项目大了,TanStack 的类型推导可能让 TS 检查变慢。几个优化方向:

  1. 别在不必要的地方让 TS 推导大类型。比如路由 loader 里调 ensureQueryData,如果返回值没人用,用 async 包一层让它返回 Promise<void>,别让 loader 的返回类型进路由树。
  2. 路由 API 传 from 收窄范围<Link to=".." /> 这种会让 TS 检查所有路由的 search params,传 from={Route.fullPath} 能收窄到当前路由分支。
  3. 别直接用 LinkProps 这种大类型。用 as const satisfies LinkProps 代替 const props: LinkProps,让类型更精确。

3.10 小结

TanStack 的类型安全靠三招:自动推导让你少写类型,queryOptions/Register 让类型穿透到全局,状态收窄让运行时判断和类型判断同步。

记住一个原则:能推导就别手写泛型。如果你发现自己在 useQuery<TData, TError> 这种写法里挣扎,先想想是不是 queryFn 的返回类型没标清楚,或者错误类型该用 Register 全局注册。

下一篇正式进入 Query,从安装和 QueryClient 开始。