- 开发工具
- 代码生成
- 后端
【免费下载链接】openapi-typescript
Generate TypeScript types from OpenAPI 3 specs
openapi-react-query 是 openapi-typescript 开源生态中的 React 数据请求层,它把 @tanstack/react-query 的能力与 OpenAPI Schema 生成的 TypeScript 类型无缝衔接,为useQuery、useMutation、useSuspenseQuery、useInfiniteQuery等常用 Hook 提供 100% 类型安全的封装。本文将以 packages/openapi-react-query/README.md 为主线,结合官方文档与仓库源码,完整讲解安装配置、五大 API 的实战用法与底层实现原理,帮助你彻底告别手写 API 类型、消灭any与as类型断言。
什么是 openapi-react-query
openapi-react-query 是一个围绕 @tanstack/react-query 的类型安全微型封装(约 1 kb),专门用于配合 OpenAPI Schema 工作(见 README.md)。它本身不直接发送网络请求,而是依赖仓库中的另外两个核心包(见 docs/openapi-react-query/index.md):
- openapi-fetch:负责实际发起 HTTP 请求的类型安全 fetch 客户端;
- openapi-typescript:负责把 OpenAPI 3 规范(YAML/JSON)编译成 TypeScript 类型定义。
三者协作后,你可以获得以下开箱即用的能力(README.md):
- ✅ URL 与参数零拼写错误:路径、查询参数全部由 Schema 类型约束;
- ✅ 参数、请求体、响应体全部经过类型检查,与你的 Schema 100% 匹配;
- ✅ 无需手动为 API 编写任何类型;
- ✅ 消除掩盖 Bug 的
any类型; - ✅ 消除同样可能掩盖 Bug 的
as类型断言。
从 package.json 可以看到,该包体积设计极轻(README 声称约 1 kb),运行时仅依赖openapi-typescript-helpers提供类型工具,而@tanstack/react-query(^5.80.0)与openapi-fetch作为 peerDependencies 由使用方安装。
安装与初始化
安装依赖
按 README.md 的指引,同时安装运行时依赖与开发期依赖:
npm i openapi-react-query openapi-fetch npm i -D openapi-typescript typescript其中:
| 包 | 角色 | 安装方式 |
|---|---|---|
openapi-react-query | 本文主角,React Query 类型安全封装 | dependencies |
openapi-fetch | 底层请求客户端,被封装对象 | dependencies |
openapi-typescript | 从 OpenAPI Schema 生成.d.ts类型 | devDependencies |
typescript | 运行生成命令与类型检查 | devDependencies |
从 Schema 生成类型
使用 openapi-typescript 的命令行工具,把 OpenAPI 文档(如v1.yaml)编译为 TypeScript 类型文件(README.md):
npx openapi-typescript ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts生成产物是一个包含paths等顶级类型的声明文件。例如仓库内的测试夹具 test/fixtures/api.d.ts 就是通过pnpm run generate-types(即openapi-typescript test/fixtures/api.yaml -o test/fixtures/api.d.ts,见 package.json)自动生成的。你可以在 packages/openapi-typescript/README.md 查看 openapi-typescript 的更多 CLI 选项(如--immutable、--export-type等)。
推荐:开启 noUncheckedIndexedAccess
官方文档强烈建议在tsconfig.json中开启 noUncheckedIndexedAccess,以及 docs/advanced.md 中的专项说明):
{ "compilerOptions": { "noUncheckedIndexedAccess": true } }基础用法:三步发起第一个请求
第一步:创建 fetch 客户端
openapi-react-query 本身不接管网络层,它接收一个 openapi-fetch 创建的客户端实例。在项目的 API 模块(如src/api.ts)中:
import createFetchClient from "openapi-fetch"; import createClient from "openapi-react-query"; import type { paths } from "./my-openapi-3-schema"; // 由 openapi-typescript 生成 const fetchClient = createFetchClient<paths>({ baseUrl: "https://myapi.dev/v1/", }); export const $api = createClient(fetchClient);这里有两层泛型传递(README.md):
createFetchClient<paths>让 openapi-fetch 获得整份 Schema 的路径、方法与参数类型;createClient(fetchClient)从 fetch 客户端上推导出paths类型,返回一个类型完备的OpenapiQueryClient。
关于createFetchClient的更多细节(baseUrl、fetch自定义、中间件等),见 docs/openapi-fetch/index.md。
第二步:在组件中使用 $api.useQuery
const MyComponent = () => { const { data, error, isPending } = $api.useQuery( "get", "/blogposts/{post_id}", { params: { path: { post_id: 5 }, }, } ); if (isPending || !data) return "Loading..."; if (error) return `An error occurred: ${error.message}`; return <div>{data.title}</div>; };这是 README.md 中的完整示例。注意三个关键点:
"get"与"/blogposts/{post_id}"都是字符串字面量类型:如果拼错 HTTP 方法或路径(例如 Schema 中不存在该路径),TypeScript 会直接报编译错误,从根本上杜绝 URL 拼写错误;params.path.post_id被推断为number:传字符串或漏传都会报错;data自动匹配 Schema 中该接口的 200 响应类型,error则匹配错误响应类型,二者完全类型化。
第三步:在 QueryClientProvider 中运行
与原生 @tanstack/react-query 一样,应用顶层需要QueryClientProvider(仓库测试在 test/index.test.tsx 中即如此包装):
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient(); export const App = () => ( <QueryClientProvider client={queryClient}> <MyComponent /> </QueryClientProvider> );五大 API 详解
createClient返回的对象包含五个方法(对应 src/index.ts 中的OpenapiQueryClient接口):queryOptions、useQuery、useSuspenseQuery、useInfiniteQuery、useMutation。测试 test/index.test.tsx 明确断言了这五个方法的存在。
useQuery:标准数据查询
useQuery与 TanStack Query 原生useQuery行为一致,但额外具备(docs/openapi-react-query/use-query.md):
- 返回值与原生
useQuery完全相同; - 自动生成的 query key 为
[method, path, params]; data与error完全类型化;- 支持第四个参数透传原生 query 选项。
完整签名(docs/openapi-react-query/use-query.md):
const query = $api.useQuery(method, path, options, queryOptions, queryClient);| 参数 | 必填 | 说明 |
|---|---|---|
method | ✅ | HTTP 方法("get"等),参与 query key 生成 |
path | ✅ | Schema 中该方法可用的路径模板,参与 query key 生成 |
options | 视 Schema 而定 | fetch 选项;仅当 Schema 要求参数时必须提供,其params参与 query key 生成 |
queryOptions | 否 | 原生useQuery的选项(enabled、select、initialData、refetchInterval等) |
queryClient | 否 | 自定义QueryClient实例(源码 src/index.ts 支持第五个可选参数) |
为什么options有时必填有时可选?源码用RequiredKeysOf<Init> extends never条件类型判断:若 Schema 中该接口没有任何必填参数,则init与 options 均可省略;若存在必填参数(如路径变量post_id),则init必须提供(src/index.ts)。测试 test/index.test.tsx 验证了「Schema 要求 params 时缺参会报编译错误」。
useMutation:写操作
useMutation用于 POST/PUT/PATCH/DELETE 等写操作,其mutationKey为[method, path](docs/openapi-react-query/use-mutation.md)。典型示例——更新用户名字:
import { $api } from "./api"; export const App = () => { const { mutate } = $api.useMutation("patch", "/users"); return ( <button onClick={() => mutate({ body: { firstname: "John" } })}> Update </button> ); };调用mutate(variables)时,variables(即 fetch 的 init 参数,含body、params等)同样被 Schema 严格约束——这里body.firstname必须匹配 Schema 中 PATCH/users的请求体定义。
签名(docs/openapi-react-query/use-mutation.md):
const mutation = $api.useMutation(method, path, queryOptions, queryClient);method/path:必填,与useQuery相同,共同构成 mutationKey;queryOptions:原生useMutation选项,如onMutate、onError、onSettled等;queryClient:可选的自定义实例。
从源码看,mutation 的mutationFn把请求失败时的error直接throw出去,成功时返回data(并排除undefined,见 src/index.ts),因此onError、error状态的处理方式与原生完全一致。测试还验证了onMutate返回值的类型在onError/onSettled回调与context中保持一致(test/index.test.tsx),并同时支持mutate与mutateAsync两种调用方式。
useSuspenseQuery:Suspense 模式查询
如果你使用 React Suspense 渲染数据,useSuspenseQuery是首选(docs/openapi-react-query/use-suspense-query.md)。它的查询 key 同样是[method, path, params],data与error完全类型化,且函数签名与useQuery完全一致(docs/openapi-react-query/use-suspense-query.md)。
import { ErrorBoundary } from "react-error-boundary"; import { $api } from "./api"; const MyComponent = () => { const { data } = $api.useSuspenseQuery("get", "/users/{user_id}", { params: { path: { user_id: 5 }, }, }); return <div>{data.firstname}</div>; }; export const App = () => ( <ErrorBoundary fallbackRender={({ error }) => `Error: ${error.message}`}> <MyComponent /> </ErrorBoundary> );Suspense 模式下组件内不再需要isPending/isLoading判断——数据未就绪时 React 会自动挂起,请求失败的错误则通过上层ErrorBoundary捕获(测试 test/index.test.tsx 用 500 响应验证了错误会正确抛给 Suspense/ErrorBoundary)。
useInfiniteQuery:无限滚动 / 分页
useInfiniteQuery在原生 API 之上额外内置了分页参数注入能力(docs/openapi-react-query/use-infinite-query.md)。典型的分页列表示例:
const PostList = () => { const { data, fetchNextPage, hasNextPage, isFetching } = $api.useInfiniteQuery( "get", "/posts", { params: { query: { limit: 10 }, }, }, { getNextPageParam: (lastPage) => lastPage.nextPage, initialPageParam: 0, } ); return ( <div> {data?.pages.map((page, i) => ( <div key={i}> {page.items.map((post) => ( <div key={post.id}>{post.title}</div> ))} </div> ))} {hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetching}> {isFetching ? "Loading..." : "Load More"} </button> )} </div> ); };签名(docs/openapi-react-query/use-infinite-query.md):
const query = $api.useInfiniteQuery( method, path, options, infiniteQueryOptions, queryClient );infiniteQueryOptions相比原生useInfiniteQuery选项,额外多出一个专属字段:
pageParamName(默认"cursor"):分页查询参数的名称。openapi-react-query 会在每次请求时自动把当前页游标注入到 URL query 中。
底层实现(src/index.ts)做了三件事:
- 解构出
pageParamName(默认"cursor"),其余选项透传给原生useInfiniteQuery; queryFn中合并init参数,把[pageParamName]: pageParam写入params.query,同时透传signal用于请求取消;- 首次请求
pageParam默认为0。
测试对此有精确验证:请求/paginated-data?limit=3时,第一页自动带上cursor=0,fetchNextPage()后第二页自动带上cursor=1;将pageParamName改为"follow_cursor"后,查询参数随之变为follow_cursor=0/1(test/index.test.tsx)。测试同样覆盖了select重排 pages/pageParams 及自定义返回类型等场景。
queryOptions:与任意 Query API 组合
当需要的 API 不在上述五个方法中时(如useQueries批量查询、QueryClient.fetchQuery手动预取、usePrefetchQuery预取等),queryOptions是官方推荐的扩展口(docs/openapi-react-query/query-options.md)。它返回一个完全类型化的 Query Options 对象,其中:
queryKey为[method, path, params];queryFn已内置为类型安全的 fetcher;data/error会被正确推导(docs/openapi-react-query/query-options.md)。
配合原生useQuery使用:
import { useQuery } from '@tanstack/react-query'; import { $api } from "./api"; export const App = () => { const { data, error, isLoading } = useQuery( $api.queryOptions("get", "/users/{user_id}", { params: { path: { user_id: 5 } }, }), ); if (!data || isLoading) return "Loading..."; if (error) return `An error occured: ${error.message}`; return <div>{data.firstname}</div>; };配合useQueries批量查询(例如按 ID 列表批量拉取用户):
import { useQueries } from '@tanstack/react-query'; import { $api } from "./api"; export const useUsersById = (userIds: number[]) => ( useQueries({ queries: userIds.map((userId) => ( $api.queryOptions("get", "/users/{user_id}", { params: { path: { user_id: userId } }, }) )) }) );由于每个queryOptions的 queryKey 都包含不同的params,批量查询会生成相互独立的缓存条目——测试 test/index.test.tsx 验证了传入 4 个不同查询时queryClient.isFetching()为 4,且各自data/error类型正确。
配合fetchQuery手动取数:
const data = await queryClient.fetchQuery( $api.queryOptions("get", "/blogposts/{post_id}", { params: { path: { post_id: 5 } }, }) );值得注意的细节(docs/openapi-react-query/query-options.md):useQuery与useSuspenseQuery内部都复用了queryOptions来构造 options。从源码看,queryOptions把init === undefined ? [method, path] : [method, path, init]作为queryKey(即无参数时 key 长度为 2,有参数时长度为 3,测试 test/index.test.tsx 验证了这一点),并共享同一个queryFn(src/index.ts)。这意味着同一个接口、同一组参数天然共享缓存——useQuery与fetchQuery、useQueries之间不存在 key 格式差异。
源码级原理:类型安全从何而来
阅读 src/index.ts 可以完整还原「类型安全」的实现机制:
1. QueryKey 类型
export type QueryKey< Paths extends Record<string, Record<HttpMethod, {}>>, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init = MaybeOptionalInit<Paths[Path], Method>, > = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];(src/index.ts)
它把「方法 + 路径(+ 参数)」直接编码进 query key 的类型:Method被限制为HttpMethod,Path被限制为「该 Method 下存在的路径」(来自openapi-typescript-helpers的PathsWithMethod)。所以 key 本身就是 Schema 的类型投影,拼错即编译失败。
2. 统一 queryFn 与错误处理
所有查询共享同一个 fetcher 逻辑(src/index.ts):
const queryFn = async ({ queryKey: [method, path, init], signal }) => { const mth = method.toUpperCase(); const fn = client[mth]; const { data, error, response } = await fn(path, { signal, ...(init as any) }); if (error) throw error; // 失败抛错 → error 状态 if (response.status === 204 || response.headers.get("Content-Length") === "0") { return data ?? null; // 空响应 → data 为 null } return data; };三个关键行为与测试一一对应:
- 请求失败时抛出
error:TanStack Query 会把抛出的错误放入error状态,于是data为undefined、error有值(测试 test/index.test.tsx); - 204 或
Content-Length: 0的空响应返回null:data/error均为null(测试 test/index.test.tsx); - 非空响应但 body 为
undefined:数据缺失被视为异常,data为undefined且error为Error实例(测试 test/index.test.tsx)。
3. 请求取消(AbortSignal)
queryFn会把 TanStack Query 的signal透传给 openapi-fetch,组件卸载或查询取消时自动中断请求。测试 test/index.test.tsx 验证了 unmount 后传给 fetch 的signal.aborted为true——这对大列表、快速切换路由的应用非常实用。
4. MethodResponse 类型工具
包还导出了MethodResponse<CreatedClient, Method, Path>类型(src/index.ts),用于从客户端实例反推某个接口的成功响应数据类型,方便在自定义 Hook、事件处理或非组件代码中引用:
import type { MethodResponse } from "openapi-react-query"; type Post = MethodResponse<typeof $api, "get", "/blogposts/{post_id}">; // → { title: string; body: string; publish_date?: number }测试 test/index.test.tsx 即用它断言了useQuery的data类型等价于string[]。
验证与测试体系
该包用 Vitest + @testing-library/react + MSW(Mock Service Worker)构建了完整的类型与行为测试(vitest.config.ts、test/fixtures/mock-server.ts)。pnpm test会先执行generate-types用 openapi-typescript 重新生成夹具类型(package.json),保证测试始终基于最新 Schema 编译产物。测试覆盖了:
- 类型层:错误的 method/path、缺失的必填参数、
select返回值推导、queryKey长度等均通过@ts-expect-error与expectTypeOf断言(test/index.test.tsx); - 行为层:成功/失败/空响应的
data、error状态机,mutation 的mutate/mutateAsync,无限分页的游标注入与自定义pageParamName,自定义queryClient透传等。
如果你要在自己的项目中复现这些场景,可以参照 test/fixtures/api.yaml 组织一份最小 Schema,再按上文「安装与初始化」流程生成类型并接入组件。
常见注意事项
- 版本要求:peerDependencies 要求
@tanstack/react-query@^5.80.0与openapi-fetch(workspace 版本,即同仓库当前版本),请确保使用兼容的版本组合(package.json); - 必填参数感知:
options参数「有时必填有时可选」是类型层面的动态行为,取决于 Schema 中该接口是否有必填参数,请留意编辑器提示而非死记规则; - 缓存一致性:
useQuery、useSuspenseQuery与queryOptions共享同一套 query key 规则([method, path, params]),跨 API 组合使用不会出现 key 冲突; - Suspense 需要 ErrorBoundary:
useSuspenseQuery的错误通过抛异常传播,务必在组件树上层配置 ErrorBoundary,否则请求失败会直接导致渲染崩溃; - 开启 noUncheckedIndexedAccess:官方强烈推荐,配合该库可获得最严格的空值检查体验(docs/advanced.md)。
总结
openapi-react-query 的价值在于把「Schema → 类型 → React 数据请求」这条链路完全打通:createClient从 fetch 客户端推导类型,五大 API 在继承 TanStack Query 全部能力的同时,把 URL、参数、请求体、响应体的类型检查下沉到编译期,并内置了空响应处理、请求取消、分页游标注入等贴心细节。对任何以 OpenAPI 规范维护接口的 React/React Native 项目,它都能显著减少样板代码与运行时错误,让 API 变更第一时间反映为编译错误。
进一步阅读:官方文档完整版见 docs/openapi-react-query/index.md,各 API 专项文档为 use-query.md、use-mutation.md、use-suspense-query.md、use-infinite-query.md 与 query-options.md;源码与测试分别在 src/index.ts 和 test/index.test.tsx。
- 开发工具
- 代码生成
- 后端
【免费下载链接】openapi-typescript
Generate TypeScript types from OpenAPI 3 specs
相关推荐
openapi-react-query 完整指南:基于 OpenAPI Schema 的类型安全 React Query 数据请求方案
openapi react query 完整指南:基于 OpenAPI Schema 的类型安全 React Query 数据请求方案 openapi reac
开发工具代码生成后端openapi-react-query:为 OpenAPI 构建类型安全的 TanStack Query 客户端
openapi react query:为 OpenAPI 构建类型安全的 TanStack Query 客户端 openapi react query 是 o
开发工具代码生成后端openapi-react-query 的 useSuspenseQuery:基于 OpenAPI Schema 的类型安全 Suspense 数据获取实践
openapi react query 的 useSuspenseQuery:基于 OpenAPI Schema 的类型安全 Suspense 数据获取实践 在
开发工具代码生成后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考