openapi-fetch 贡献指南:类型声明、Vitest 类型测试与 Biome 检查的最佳实践
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 的说明,准备工作非常简单:
- 安装 [pnpm](本仓库使用 pnpm 管理依赖,根目录的
pnpm-workspace.yaml与pnpm-lock.yaml即为其佐证); - Fork 仓库并在本地克隆你自己的副本;
- 在仓库根目录运行
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 的类型质量:
-
必须使用
assertType<T>(…)。不要只检查实际的运行时值,还要检查被感知到的类型。仓库中 packages/openapi-fetch/test/types.test.ts 是典型范例:它通过assertType<Response>({ data: "200 application/json" })断言成功响应内容,又用assertType<Response>({ error: "500 application/json" })断言错误响应分支,把类型形状当作一等断言来写。 -
测试 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)也遵循同一模式。 -
尽可能缩小
// @ts-expect-error的作用范围。JS 基本忽略空白,因此写// @ts-expect-error时,要把一个表达式拆成尽量多的行,让注释精确落在出错的那一行上;否则表达式里另一个部分抛出的错误也会被它吞掉,导致断言失效。 -
手动写出类型测试,避免
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等)。未经讨论的功能工作,维护者有权拒绝。
具体流程
- 创建分支:
git checkout -b your-branch-name; - 补齐代码、文档与测试:涉及类型行为改动时,务必同步更新
src/index.d.ts、src/index.js以及对应的test/**/*.test.ts; - 推送并开 PR:
git push后回到你的 GitHub 仓库页,会出现创建 PR 的提示; - 认真填写 PR 模板:模板很轻量,但请务必填写;
- 更新文档:如果你新增或改变了某个功能的工作方式,请同步更新根目录下的 docs/ 文档(原贡献指南中的
../../docs/即指向仓库根目录的docs/,其中docs/openapi-fetch/下就有该库的 API、示例、中间件与鉴权、测试等章节); - 通过 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 最核心的“类型推断”承诺在每一行新代码上持续成立。