openapi-fetch 贡献指南:类型声明、Vitest 类型测试与 Biome 检查的最佳实践

原创2026-09-25 12:11:57371 阅读
文章标签:开发工具代码生成后端

openapi-fetch 贡献指南:类型声明、Vitest 类型测试与 Biome 检查的最佳实践

openapi-fetch 是 openapi-typescript 仓库中面向运行时的类型安全 fetch 客户端,其代码库的核心难点在于“从 OpenAPI Schema 推导复杂 TypeScript 类型”。本文基于 packages/openapi-fetch/CONTRIBUTING.md 展开,完整讲解该项目的贡献流程:环境搭建、手动类型声明与运行时代码分离的架构约定、以 assertType<T>() 与 // @ts-expect-error 为核心的类型测试规范、Biome 代码检查、changesets 变更日志生成,以及从开 PR 到通过 CI 的全流程。读完本文,你将能独立为 openapi-fetch 提交高质量、类型正确且通过全部检查的代码。

为什么贡献 openapi-fetch:先理解这个“小而巧”的库

在动手改代码前,先了解这个包在整个 openapi-typescript 生态中的位置。openapi-fetch 是一个体积约 6 kB(min)的类型安全 fetch 客户端,它直接消费 openapi-typescript 从 OpenAPI 3 规范生成的 paths 类型,并提供 GET、PUT、POST 等与原生 fetch API] 语义对齐的方法,适用于 React、Vue、Svelte 或原生 JS(见 [packages/openapi-fetch/package.json 的 description 与 README)。

这个库存在的意义是正确的类型推断,而不是复杂的运行时逻辑。因此,对它的贡献几乎总是围绕“类型行为”展开:某个参数是否可选、某个响应在 2XX 与 4XX/5XX 下如何区分 data 与 error、readOnly/writeOnly 属性如何在不同场景下被剔除。理解这一点,是读懂下面所有约定(尤其是“类型声明与运行时分离”)的前提。

环境搭建:三步准备开发环境

按 CONTRIBUTING.md 的说明,准备工作非常简单:

  1. 安装 [pnpm](本仓库使用 pnpm 管理依赖,根目录的 pnpm-workspace.yaml 与 pnpm-lock.yaml 即为其佐证);
  2. Fork 仓库并在本地克隆你自己的副本;
  3. 在仓库根目录运行 pnpm i 安装全部依赖(openapi-fetch 依赖 openapi-typescript-helpers 工作区包,以及 devDependencies 中的 openapi-typescript、vitest、typescript、vite 等,见 packages/openapi-fetch/package.json)。

提示:openapi-fetch 的 pretest 脚本会先执行 pnpm run generate-types(即 openapi-typescript -c test/redocly.yaml),用仓库内的 redocly 配置重新生成测试 schema 的类型文件。因此首次拉取代码后,直接运行测试前依赖安装是完整且必需的。

核心架构约定:index.d.ts 与 index.js 的“双文件”设计

这是本贡献指南中最关键的一条约定,也是新手最容易踩坑的地方。

openapi-fetch 刻意使用手动类型声明(即 .d.ts 文件),且类型声明与运行时代码物理分离:

  • src/index.d.ts:存放类型声明。这是最重要的代码,因为该库的全部价值都在于正确的类型推断。文件中定义了 ClientOptions、FetchOptions、FetchResponse、Client、Middleware、QuerySerializer 等一整套类型面(见 packages/openapi-fetch/src/index.d.ts);
  • src/index.js:存放运行时代码。它只做支撑 API 所需的最小工作,例如 URL 拼接、query/body/path 序列化、middleware 编排、content-type 头处理等(见 packages/openapi-fetch/src/index.js)。

为什么这样设计?贡献指南给出了坦率的解释:在大多数项目中这种分离并不被推荐——两套代码可能漂移,最终类型会“欺骗”你关于运行时行为的认知。但 openapi-fetch 是个例外:由于类型推断需要基于用户提供的 schema 做极复杂的条件类型运算(如 ParamsOption<T> 根据 RequiredKeysOf<T["parameters"]> 决定 params 是否必填,RequestBodyOption<T> 通过 IsOperationRequestBodyOptional<T> 决定 body 是否可选),若与可读的运行时代码混在一起,复杂类型会严重干扰运行时代码的编写与维护。分离之后,更复杂的 TypeScript 部分不再影响最优化的运行时实现。这一取舍在小而精的代码库中完全可行,但换到更大的项目就难以为继。

改 index.js 必须同步改 index.d.ts

写作代码时,人们很容易只改 index.js(因为更简单),但请不要这样做。测试文件(*.test.ts)被有意设计成让类型推断与运行时同时被 typecheck。只要你改了 index.js,大概率也需要同步修改 index.d.ts,并且在测试中验证类型正确性与验证运行时同样重要。

从源码可以印证两者的配合:src/index.js 的 createClient() 解构了 baseUrl、fetch、querySerializer、bodySerializer、pathSerializer、headers、requestInitExt 等选项,而这些选项的合法形态正是由 src/index.d.ts 中的 ClientOptions 定义的;coreFetch() 内部对 parseAs(默认 "json")、bodySerializer、middleware 的处理,也与 FetchOptions、MergedOptions、Middleware 等类型一一对应。改运行时行为时,如果类型面没有跟上,测试的 typecheck 阶段就会立刻暴露不一致。

测试:Vitest 与“类型即断言”的测试哲学

openapi-fetch 使用 Vitest] 作为测试框架,并可选安装 VS Code 的 Vitest Explorer 扩展以获得编辑器内调试能力(配置见 [packages/openapi-fetch/vitest.config.ts,其中 typecheck: { enabled: true } 意味着类型检查是测试的一部分)。

常用命令

运行整个测试套件:

pnpm test

注意:package.json 中 test 脚本为 pnpm run test:js && pnpm run test:exports,前者执行 vitest run,后者会先构建再对包导出做检查(attw --pack .),所以 pnpm test 是包含构建与导出校验的完整流程。

运行单个测试文件(按文件名片段过滤):

pnpm test -- [partial filename]

例如想只跑 types.test.ts,可传 pnpm test -- types。

以监视模式启动整个测试套件:

npx vitest

类型测试的四大铁律

贡献指南用专门篇幅强调测试写法,这些规则直接决定了 openapi-fetch 的类型质量:

  1. 必须使用 assertType<T>(…)。不要只检查实际的运行时值,还要检查被感知到的类型。仓库中 packages/openapi-fetch/test/types.test.ts 是典型范例:它通过 assertType<Response>({ data: "200 application/json" }) 断言成功响应内容,又用 assertType<Response>({ error: "500 application/json" }) 断言错误响应分支,把类型形状当作一等断言来写。

  2. 测试 TS 报错与测试期望类型同样重要。多用 // @ts-expect-error。日常开发中它常被视为“藏错误”,但在本项目里,我们恰恰想要验证非法输入会触发 TS 错误。同样是 types.test.ts,大量使用 // @ts-expect-error 断言“200 但不匹配字面量”“204 never 不会变成 undefined”等边界;其余测试文件(如 packages/openapi-fetch/test/http-methods/post.test.ts、packages/openapi-fetch/test/common/params.test.ts)也遵循同一模式。

  3. 尽可能缩小 // @ts-expect-error 的作用范围。JS 基本忽略空白,因此写 // @ts-expect-error 时,要把一个表达式拆成尽量多的行,让注释精确落在出错的那一行上;否则表达式里另一个部分抛出的错误也会被它吞掉,导致断言失效。

  4. 手动写出类型测试,避免 test.each 用于类型断言。test.each 参数化容易掩盖错误(例如把类型断言与运行时数据混在参数表里,某一组合的类型错误会被模糊掉)。从仓库现状看,类型断言均以显式 test("...", () => { assertType<...>(...) }) 形式书写。

另外,测试目录中还包含 no-strict-null-checks 子目录(packages/openapi-fetch/test/no-strict-null-checks/ 及其 tsconfig.json),专门验证库在关闭 strictNullChecks 的工程下仍能工作,这也是 pnpm run lint:ts-no-strict 脚本(tsc --noEmit -p test/no-strict-null-checks/tsconfig.json)存在的原因——改类型代码时请留意这类非严格模式下的兼容性。

运行时测试同样围绕类型面展开

即使是“运行时”测试,也常以类型面为观察对象。例如 packages/openapi-fetch/test/common/create-client.test.ts 用观察型 fetch 捕获实际请求 URL,验证 baseUrl 的拼接与尾部斜杠去除行为,以及请求体存在时默认 Content-Type: application/json、用户显式覆盖 header 时优先级生效等细节——这些行为的类型契约正是 ClientOptions、HeadersOptions 所声明的。可见“类型正确性”与“运行时正确性”在这套测试体系里是互为表里的。

代码检查:Biome

代码检查由 [Biome] 负责,它是更快的 ESLint 替代品。依赖通过 pnpm i 一并安装,运行:

pnpm run lint

在 packages/openapi-fetch/package.json 中,lint 实际由三个子任务组成:

pnpm run lint:js      # biome check .
pnpm run lint:ts      # tsc --noEmit
pnpm run lint:ts-no-strict  # tsc --noEmit -p test/no-strict-null-checks/tsconfig.json

也就是说,“lint”不仅指静态检查工具,还包含两次 tsc 类型检查(严格模式与关闭 strictNullChecks 的模式)。Biome 的配置在 packages/openapi-fetch/biome.json:它 extends 仓库根配置,并针对本包做了小幅放宽(关闭 noBannedTypes 与 noConfusingVoidType),同时排除了 examples、schemas 与压缩的 benchmark 文件。修改代码后,biome check . 与两套 tsc --noEmit 全部通过是提交的基本门槛。

变更日志:changesets

openapi-fetch 的变更日志由 [changesets] 生成,与 Git commit message、PR 标题完全独立。要为你的改动写一份人可读的 changelog,运行:

npx changeset

该命令会交互式询问:这是 patch、minor 还是 major 变更(遵循 semver]),以及一段对你的改动的平实描述。随后把生成的变更集文件连同 PR 一起提交,下一次发布时它就会进入官方 CHANGELOG(见 [packages/openapi-fetch/CHANGELOG.md)。

也正因如此,贡献指南明确:仓库不强制特定 commit message 风格(虽鼓励以祈使动词开头、保持简短、必要时使用正文),更看重清晰度而非僵化规则——因为可读的版本历史由 changesets 单独产出。

提交 Pull Request:从分支到合并

PR 在本仓库是受欢迎的,但有不同的审查尺度:

  • Bugfix 总是会被接受,个别情况下可能被要求做小幅调整;
  • 新增功能或破坏性变更,请先开 issue 讨论,再动手写代码。这能避免把时间浪费在最终不会被项目接受的实现上(参考项目目标文档 docs/about.md 等)。未经讨论的功能工作,维护者有权拒绝。

具体流程

  1. 创建分支:git checkout -b your-branch-name;
  2. 补齐代码、文档与测试:涉及类型行为改动时,务必同步更新 src/index.d.ts、src/index.js 以及对应的 test/**/*.test.ts;
  3. 推送并开 PR:git push 后回到你的 GitHub 仓库页,会出现创建 PR 的提示;
  4. 认真填写 PR 模板:模板很轻量,但请务必填写;
  5. 更新文档:如果你新增或改变了某个功能的工作方式,请同步更新根目录下的 docs/ 文档(原贡献指南中的 ../../docs/ 即指向仓库根目录的 docs/,其中 docs/openapi-fetch/ 下就有该库的 API、示例、中间件与鉴权、测试等章节);
  6. 通过 CI:所有 PR 必须修复 lint 错误且全部测试通过,CI 检查全部为 “green”(✅)之前不会被合并。

小结:贡献 openapi-fetch 的检查清单

当你准备提交代码时,可对照以下清单自查:

  • [ ] pnpm i 后依赖完整,能运行 pnpm test(含 vitest run 与构建导出检查)
  • [ ] 运行时改动(src/index.js)是否同步修改了类型声明(src/index.d.ts)
  • [ ] 测试中使用 assertType<T>() 验证感知类型,而不只验证运行时值
  • [ ] 对非法输入使用精确作用范围的 // @ts-expect-error,并拆分表达式使注释落在正确行
  • [ ] 类型测试手动书写,未用 test.each 掩盖错误
  • [ ] pnpm run lint 三项(Biome、严格 tsc、非严格 tsc)全部通过
  • [ ] 行为变化已通过 npx changeset 生成变更集
  • [ ] 功能/破坏性变更已提前在 issue 中讨论;文档(docs/)已同步
  • [ ] PR 模板已填写,CI 全绿

遵循这些约定,你的改动不仅能在运行时正确工作,还能被 TypeScript 编译器严格验证,让 openapi-fetch 最核心的“类型推断”承诺在每一行新代码上持续成立。

说明:文中引用的源码路径均位于本仓库内,可按需深入阅读 类型声明实现、运行时实现、类型测试范例 与 客户端选项测试。

登录后查看全文
openapi-typescript