
在 Convex 中使用 TypeScript exactOptionalPropertyTypes 编写类型安全的 query 与 mutation 函数【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址: https://gitcode.com/gh_mirrors/co/convex-backend本指南以 convex-backend 仓库中的typescript-exact-optional-property-types演示项目为主体讲解 Convex 服务端函数query / mutation的标准编写方式并深入剖析在开启 TypeScript 严格编译选项exactOptionalPropertyTypes: true后如何正确处理带可选字段v.optional(...)的文档类型。读完本文你将掌握 Convex 函数目录的完整组织方式、参数校验器Validator用法、React 端调用范式以及可选属性类型在数据模型中的源码级实现原理能够直接在自己的 Convex 项目中复刻这套严格类型配置。演示项目定位Convex 函数目录的标准模板与严格类型实验场在 convex-backend 仓库中npm-packages/private-demos/typescript-exact-optional-property-types/ 是一个private demo私有演示项目。与仓库内其他 demo 一样它被用来验证在特定 TypeScript 配置下 Convex 的类型行为。该目录下的 README.md 只有一句话This is a recent TypeScript version with the tsconfig.json optionexactOptionalPropertyTypes: trueset.即这是一个安装了较新 TypeScript 版本、并在 tsconfig.json 中开启了exactOptionalPropertyTypes: true的演示项目用于验证 Convex 的校验器类型推断在该严格选项下是否依然精确。而convex/子目录下的 README.md 则是 Convex 官方为每个项目自动生成的函数目录说明文档它记录了编写 Convex 服务端函数的标准姿势。两者结合正好构成一篇标准用法 严格类型边界的完整技术指南。项目的整体结构如下npm-packages/private-demos/typescript-exact-optional-property-types/ ├── README.md # 项目说明开启 exactOptionalPropertyTypes ├── package.json # 依赖 convexworkspace:*、typescript ^5.9.2 ├── tsconfig.json # 项目级严格 TS 配置 ├── turbo.json └── convex/ # Convex 函数目录 ├── README.md # 官方函数编写指南关联文档 ├── messages.ts # 演示查询函数展示可选属性类型行为 ├── schema.ts # 数据模型含可选字段的表定义 ├── tsconfig.json # 函数运行环境的 TS 配置 └── _generated/ # 自动生成代码api / dataModel / serverConvex 函数目录一切从convex/开始Convex 要求所有服务端函数都放在项目根目录的convex/文件夹下。每一个导出的函数都会自动成为应用公共 API 的一部分客户端通过类型安全的引用如api.myFunctions.myQueryFunction调用它们无需手写 URL 路由或 HTTP 端点。目录中的 convex/tsconfig.json 描述了 Convex 函数的运行环境并用于对这些函数做类型检查。其中有几个由 Convex 强制要求的编译选项不能随意修改{ compilerOptions: { target: ESNext, lib: [ES2023, dom], forceConsistentCasingInFileNames: true, module: ESNext, isolatedModules: true, noEmit: true } }noEmit: trueConvex 函数由 CLI 负责打包与部署本地不做编译产物输出isolatedModules: true保证每个文件可被独立转译适配 Convex 的分文件编译模型module: ESNext与target: ESNext匹配 Convex 运行时的现代 JavaScript 能力。同时这份配置也允许你自行追加严格选项如strict: true在函数侧获得更严格的类型检查。编写第一个 query 函数参数校验器与 handler关联文档给出的 query 函数标准模板如下完整继承自 convex/README.md// convex/myFunctions.ts import { query } from ./_generated/server; import { v } from convex/values; export const myQueryFunction query({ // Validators for arguments. args: { first: v.number(), second: v.string(), }, // Function implementation. handler: async (ctx, args) { // Read the database as many times as you need here. const documents await ctx.db.query(tablename).collect(); // Arguments passed from the client are properties of the args object. console.log(args.first, args.second); // Write arbitrary JavaScript here: filter, aggregate, build derived data, // remove non-public properties, or create new objects. return documents; }, });几个关键点query从./_generated/server导入这不是手写代码而是由npx convex dev自动生成。查看 convex/_generated/server.d.ts 可以看到query被声明为QueryBuilderDataModel, public其类型参数DataModel直接来自你的 schema从而保证ctx.db上的所有表名与文档类型都被静态校验。args用 Validator 描述参数形状v.number()、v.string()来自convex/values。运行时会据此校验入参编译期则据此推断args的 TypeScript 类型。handler是纯异步函数ctx提供数据库读取能力args是经过校验的客户端参数对象。你可以在 handler 中做任意数据变换过滤、聚合、派生数据、剔除非公开字段最后返回给客户端。在 React 组件中调用 queryconst data useQuery(api.myFunctions.myQueryFunction, { first: 10, second: hello, });useQuery接收两个参数函数引用api.xxx与参数对象。由于api来自自动生成的 convex/_generated/api.d.ts其类型为ApiFromModules{ messages: typeof messages }参数对象的字段、类型乃至可选性都会与服务器端args校验器严格对齐——写错参数名或类型会直接在编译期报错。编写 mutation 函数原子写入数据库mutation 与 query 结构相同区别在于它可以写数据库且 Convex 保证一次 mutation 内的所有写入原子生效不会出现半写状态// convex/myFunctions.ts import { mutation } from ./_generated/server; import { v } from convex/values; export const myMutationFunction mutation({ // Validators for arguments. args: { first: v.string(), second: v.string(), }, // Function implementation. handler: async (ctx, args) { // Insert or modify documents in the database here. // Mutations can also read from the database like queries. const message { body: args.first, author: args.second }; const id await ctx.db.insert(messages, message); // Optionally, return a value from your mutation. return await ctx.db.get(messages, id); }, });客户端侧使用useMutation触发const mutation useMutation(api.myFunctions.myMutationFunction); function handleButtonPress() { // fire and forget, the most common way to use mutations mutation({ first: Hello!, second: me }); // OR // use the result once the mutation has completed mutation({ first: Hello!, second: me }).then((result) console.log(result), ); }两种调用方式分别对应无需关心结果fire and forget与等待提交完成并消费返回值两种场景。返回值会经过序列化传输给客户端其类型同样由 Validator 推断。数据模型用v.optional声明可选字段本 demo 的核心实验对象是 convex/schema.ts 中定义的messages表import { defineSchema, defineTable } from convex/server; import { v } from convex/values; export default defineSchema({ messages: defineTable({ author: v.string(), body: v.string(), optionalString: v.optional(v.string()), objectWithOptionalString: v.object({ optionalString: v.optional(v.string()), }), }), });这里展示了两种可选形态顶层可选字段optionalString: v.optional(v.string())文档可以没有该字段嵌套对象中的可选字段objectWithOptionalString一个对象值内部又包含可选的字符串字段。这张表被convex/_generated/dataModel.d.ts中的DataModelFromSchemaDefinitiontypeof schema消费自动推导出messages表的文档类型Docmessages。也就是说schema 是数据模型的唯一事实来源函数里的ctx.db类型全部由此派生。exactOptionalPropertyTypes 到底改变什么先看 TypeScript 层面的语义差异。默认情况下可选属性optionalString?: string的类型被推断为string | undefined并且显式赋值undefined是合法的——此时字段存在但值为 undefined。而开启exactOptionalPropertyTypes: true后可选属性optionalString?: string表示要么该属性完全不存在要么它是一个真正的string把undefined赋给可选属性会被视为类型错误除非属性类型显式写作string | undefined。这正是 tsconfig.json 中注释所强调的该配置参考了 TypeScript 官方 PR 的推荐模板{ compilerOptions: { // Stricter Typechecking Options noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true, // Recommended Options strict: true, jsx: react-jsx, verbatimModuleSyntax: true, isolatedModules: true, moduleDetection: force, // the convex package doesnt typecheck when using exactOptionalPropertyTypes skipLibCheck: true, noEmit: true, forceConsistentCasingInFileNames: true } }值得注意的配套选项noUncheckedIndexedAccess: true数组索引访问会得到T | undefined这解释了 demo 代码中stuff[0]!的非空断言写法skipLibCheck: true注释明确说明convex 包在 exactOptionalPropertyTypes 下无法完整通过类型检查因此跳过node_modules中.d.ts的检查只对自己的业务代码严格把关verbatimModuleSyntax与moduleDetection: force配合module: nodenext保证 ESM 语义下的导入/导出形式与模块识别正确所以messages.ts中导入使用./_generated/server.js后缀。源码级原理Convex 如何兼容 optional 字段Convex 的v.optional(...)之所以能在exactOptionalPropertyTypes下生成精确类型关键在于 npm-packages/convex/src/values/validator.ts 中ObjectType类型的实现export type ObjectTypeFields extends PropertyValidators Expand // Map each key to the corresponding property validators type making // the optional ones optional. { // This Exclude..., undefined does nothing unless // the tsconfig.json option exactOptionalPropertyTypes: true, // is used. When it is it results in a more accurate type. // When it is not the Exclude removes undefined but it is // added again by the optional property. [Property in OptionalKeysFields]?: Exclude InferFields[Property], undefined ; } { [Property in RequiredKeysFields]: InferFields[Property]; } ;这段源码注释本身就是为exactOptionalPropertyTypes场景专门写的揭示了精妙之处OptionalKeysFields从属性校验器中挑出isOptional optional的键RequiredKeys挑出其余必填键可选键被映射为?: ExcludeInfer..., undefined——先剔除undefined再标记为可选属性在默认未开启exactOptionalPropertyTypes模式下可选属性?: T本身就被推断为T | undefinedExclude剔除undefined后又被可选属性加回来所以结果不变、行为兼容在开启exactOptionalPropertyTypes后?: T不再隐式包含undefined此时预先用Exclude剔除的undefined不会被加回最终类型精确为属性可缺席但一旦存在就是纯string。也就是说同一份 Validator 代码在两种编译选项下都能产出正确的类型这正是该 demo 要验证的兼容性保证。实战验证demo 查询函数中的类型收窄模式convex/messages.ts 中的list查询函数是整套配置的活体测试用例它系统地展示了在exactOptionalPropertyTypes下如何安全消费可选字段import { query } from ./_generated/server.js; export const list query({ args: {}, handler: async (ctx) { const stuff await ctx.db.query(messages).collect(); // (noUncheckedIndexedAccess) const doc stuff[0]!; // exactOptionalPropertyTypes isnt any different when you access this const optionalField: undefined | string doc.optionalString; console.log(optionalField); const { _id, _creationTime, body: _body, author: _author, objectWithOptionalString, ...justOptional } doc; if (optionalString in justOptional) { const exists: string justOptional.optionalString; console.log(exists); } else { const dne: undefined justOptional.optionalString; // ts-expect-error undefined is not assignable to string const exists: string justOptional.optionalString; console.log(dne, exists); } if (optionalString in objectWithOptionalString) { // ts-expect-error building convex with exact-optional-property-types fixes this const exists: string justOptional.optionalString; console.log(exists); } else { // ts-expect-error building convex with exact-optional-property-types fixes this const dne: undefined justOptional.optionalString; // ts-expect-error undefined is not assignable to string const exists: string justOptional.optionalString; console.log(dne, exists); } }, });从中可以提炼出几条可直接复用的模式直接属性访问doc.optionalString的类型是undefined | string注释说明此时exactOptionalPropertyTypes并不会带来差异与默认模式行为一致解构 in操作符收窄将文档解构出justOptional剔除_id、_creationTime及必填字段后再用optionalString in justOptional做运行时存在性检查。存在分支里justOptional.optionalString被收窄为纯string可直接赋给string类型不存在分支里它是undefined赋给string会触发ts-expect-error——这正是exactOptionalPropertyTypes严格语义的直接体现值必须属性存在且为 string或属性缺席二选一嵌套对象场景的已知问题objectWithOptionalString分支中的注释写明building convex with exact-optional-property-types fixes this使用 exactOptionalPropertyTypes 构建 convex 会修复此问题说明 demo 同时记录了嵌套可选对象在类型推断上仍存在的边界情况并用ts-expect-error显式标注保证tsc构建npm run build即tsc在严格配置下依然通过。运行与验证CLI 工作流关联文档最后给出了完整的命令行工作流本 demo 的 package.json 也与之对应# 在项目根目录启动本地开发自动同步函数并生成 _generated 代码 npx convex dev # 查看 Convex CLI 的全部能力 npx convex -h # 启动本地文档服务 npx convex docsnpx convex dev会监听convex/目录将函数推送到本地开发部署并重新生成 convex/_generated/ 下的api.d.ts、dataModel.d.ts、server.d.ts等文件文件头部均注明 To regenerate, runnpx convex dev本 demo 的package.json还提供了npm run dev即convex dev与npm run build即tsc两个脚本后者用于在 CI 或本地验证整个项目含messages.ts中的ts-expect-error断言在exactOptionalPropertyTypes配置下类型检查通过依赖方面项目通过convex: workspace:*直接引用 monorepo 内的 convex 包并搭配typescript: ^5.9.2exactOptionalPropertyTypes自 TypeScript 4.4 起可用5.x 完全支持。小结以 convex/README.md 的标准模板为基础结合typescript-exact-optional-property-types这个演示项目可以总结出一条清晰的实践路径函数即 API在convex/目录用query/mutation包裹 handler用v.*校验器描述参数与文档结构客户端通过生成的api/internal引用获得端到端类型安全schema 驱动类型v.optional(...)声明可选字段后_generated/dataModel.d.ts会推导出精确的文档类型严格选项可选但值得开启在 tsconfig 中追加exactOptionalPropertyTypes: true配合noUncheckedIndexedAccess、strict、skipLibCheck并用in 操作符做存在性收窄即可在属性缺席与属性为 undefined之间获得严格区分兼容性由底层保证validator.ts 中ObjectType的ExcludeInfer..., undefined技巧让同一套 Validator 在两种编译选项下都产出正确类型这正是该 demo 存在并被纳入仓库持续验证的价值所在。如果你想在自己的 Convex 项目中复刻这套严格类型配置直接参考本 demo 的 项目级 tsconfig.json 与 convex/tsconfig.json并把convex/下的 schema 与函数模板迁移过去即可。【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址: https://gitcode.com/gh_mirrors/co/convex-backend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考