在 Convex 后端项目中使用 TypeScript exactOptionalPropertyTypes 的完整实践指南
【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址: https://gitcode.com/gh_mirrors/co/convex-backend
导读
exactOptionalPropertyTypes是 TypeScript 5.0 起引入的严格类型检查选项,它改变了可选属性(prop?: T)的类型语义:读取时属性类型不再隐式包含undefined,写入时必须显式区分"属性缺失"与"属性值为undefined"。本指南以 convex-backend 仓库中的typescript-exact-optional-property-types示例项目为骨架,结合其tsconfig.json、Convex 函数源码与自动生成的类型文件,讲解如何在 Convex 应用中开启该选项、它对v.optional()校验器生成的文档类型有何影响,以及如何借助noUncheckedIndexedAccess、skipLibCheck等配套配置获得更严格的类型安全。
一、示例项目定位:验证严格类型模式下的 Convex 开发体验
仓库中的 npm-packages/private-demos/typescript-exact-optional-property-types/README.md 对该示例的定位非常精炼:"This is a recent TypeScript version with the tsconfig.json optionexactOptionalPropertyTypes: trueset."——即使用较新版本的 TypeScript(项目声明依赖typescript: ^5.9.2,见 package.json),并在 tsconfig 中开启exactOptionalPropertyTypes的演示项目。
它属于仓库中 private-demos 目录下的一组"类型能力验证"型示例(同目录还包含typescript-modern、typescript-old、typescript-exact-optional-property-types等,分别用于验证不同 TS 版本与编译选项下的 Convex 兼容性)。这类示例的价值在于:用最小可运行的项目,验证某个激进 TypeScript 配置是否与 Convex 的 schema 校验器、自动生成类型(convex/_generated)以及convex dev工作流兼容。
项目结构如下:
npm-packages/private-demos/typescript-exact-optional-property-types/ ├── convex/ │ ├── _generated/ # npx convex dev 自动生成的类型(api.d.ts、dataModel.d.ts 等) │ ├── messages.ts # 演示 exactOptionalPropertyTypes 行为的 query 函数 │ ├── schema.ts # 定义了含可选字段的 messages 表 │ └── tsconfig.json # Convex 函数目录自身的 TS 配置 ├── package.json # scripts: dev = convex dev,build = tsc ├── tsconfig.json # 根配置:开启 exactOptionalPropertyTypes 等严格选项 └── turbo.json # turbo build 任务:仅类型检查、无产物输出其中turbo.json明确声明"build"任务的"outputs": [],并在注释中说明"The build script only typechecks; it emits no files"——也就是说,这个示例的build脚本(即tsc)只用于类型检查,不产出编译文件(根 tsconfig 中亦配置了"noEmit": true)。
二、根 tsconfig.json 逐项解读:如何组合出"极致严格"的配置
示例项目的根 tsconfig.json 并非随手打开几个开关,而是一套经过推敲的严格配置组合,其注释甚至给出了出处(约等于 microsoft/TypeScript PR #61813 所讨论的推荐配置)。逐项拆解如下:
2.1 环境设置
"module": "nodenext", "target": "esnext", "lib": ["esnext"], "types": ["node"], "sourceMap": true, "declaration": false, "declarationMap": falsemodule: "nodenext"配合target: "esnext"适用于 Node.js 端的现代 ESM 工程;lib: ["esnext"]只引入 ESNext 标准库,types: ["node"]引入 Node 类型(需npm install -D @types/node,示例 devDependencies 中已包含@types/node: ^18.17.0)。sourceMap: true保留调试能力。declaration/declarationMap显式关闭,配置注释说明了原因:"This doesn't work with the inferred types of convex functions"——Convex 函数(query/mutation)的返回类型依赖运行时推断,declaration与其不兼容。这是一个从源码配置中可以确认的重要事实:在开启严格选项的 Convex 工程中,不要试图开启.d.ts产物生成。
2.2 严格类型检查选项(本示例的核心)
"noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "strict": truestrict: true是 TypeScript 推荐的基础严格模式。noUncheckedIndexedAccess: true:对数组/对象的索引访问(如stuff[0])将额外叠加undefined,强制开发者处理越界与缺失情况。exactOptionalPropertyTypes: true:本示例的主角。开启后,声明为prop?: string的属性,其读取类型不再包含undefined;同时,给可选属性赋undefined会报错,除非该属性类型本就声明为string | undefined。
2.3 推荐选项与工程配套
"jsx": "react-jsx", "verbatimModuleSyntax": true, "isolatedModules": true, "noUncheckedSideEffectImports": true, "moduleDetection": "force", "skipLibCheck": true, "noEmit": true, "forceConsistentCasingInFileNames": trueverbatimModuleSyntax+isolatedModules:保证按源码原样保留导入语法、且每个文件可独立编译,是跨 bundler / NodeNext 工程的一致选择。noUncheckedSideEffectImports:对仅用于副作用导入的文件做存在性检查(TS 5.6+ 选项)。skipLibCheck: true值得特别说明:配置注释明确指出"the convex package doesn't typecheck when using exactOptionalPropertyTypes"——即convex npm 包自身的类型声明(.d.ts)在exactOptionalPropertyTypes下无法通过类型检查,因此必须开启skipLibCheck跳过对依赖库声明文件的检查。这是从示例配置中直接可证的关键经验:第三方库类型尚未适配该严格选项时,skipLibCheck是必要的妥协手段。
三、schema 侧的准备:用 v.optional() 定义可选字段
Convex 的 schema 通过校验器(validators)描述数据结构。示例的 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())——该字段在文档中可以缺失,值为string或缺失。 - 嵌套对象中的可选字段:
objectWithOptionalString是一个v.object,其内部同样含有v.optional(v.string())字段,用于验证exactOptionalPropertyTypes对嵌套校验器类型推断的影响。
值得强调的是,v.optional(...)生成的可选字段与 TypeScript 的?:可选属性在语义上天然呼应:schema 的"字段可缺失"对应 TS 的"属性可缺失"。这正是exactOptionalPropertyTypes能在此类项目中产生连锁影响的原因——Convex 会根据 schema 生成Doc类型(见下文),可选字段会被映射为可缺省属性。
四、函数代码实测:exactOptionalPropertyTypes 在文档访问中的行为差异
示例的核心演示代码在 convex/messages.ts,一个名为list的 query 函数。它以ctx.db.query("messages").collect()取出全部文档,随后围绕可选字段做了细致的类型验证,代码中的@ts-expect-error注释本身就是"行为断言",值得逐段分析:
4.1 配合 noUncheckedIndexedAccess 的数组访问
const stuff = await ctx.db.query("messages").collect(); // (noUncheckedIndexedAccess) const doc = stuff[0]!;在noUncheckedIndexedAccess下,stuff[0]的类型是Doc<"messages"> | undefined,因此示例用非空断言!显式声明"这里一定有元素"。注释直接标注了该行为由noUncheckedIndexedAccess引起。
4.2 可选字段的读取:类型中不再隐式携带 undefined
// exactOptionalPropertyTypes isn't any different when you access this const optionalField: undefined | string = doc.optionalString;代码注释明确说明:"exactOptionalPropertyTypes isn't any different when you access this"——读取可选属性时,其类型与未开启该选项时没有区别(仍是string | undefined),因为 Convex 生成的可选字段类型本就如此。这一结论很重要:exactOptionalPropertyTypes的差异主要体现在赋值与可选属性与undefined的区分,而非读取端。
4.3 用解构 + in 操作符区分"缺失"与"存在"
示例通过解构把文档拆成三部分:
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); }- 通过 rest 解构得到的
justOptional只含optionalString一个可选字段。 - 用
in操作符做存在性收窄:命中if分支时,justOptional.optionalString收窄为string;进入else分支时它被收窄为undefined(即"属性不存在"),此时再把它赋给string就会触发@ts-expect-error断言。这一模式展示了在严格选项下安全访问可选属性的推荐写法。
4.4 嵌套可选字段的已知限制(demo 的核心结论)
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); }注意这段代码的三个@ts-expect-error断言,其注释揭示了一个已知缺陷:当前 Convex 根据 schema 生成文档类型时,嵌套对象(objectWithOptionalString)内的可选字段在exactOptionalPropertyTypes下没有被完整建模——justOptional在else分支中本应收窄为undefined,但断言注释写道"building convex with exact-optional-property-types fixes this",即期望未来 Convex 构建链适配该选项后消除此类误报。从源码结构看,这是示例作者有意留下的"待改进标记":它验证了「Convex 生成类型在严格可选属性语义下的边界」。
五、Convex 自动生成的类型:exactOptionalPropertyTypes 的落点
运行npx convex dev后,CLI 会在 convex/_generated 目录生成类型文件。其中 dataModel.d.ts 是理解上文行为的关键:
export type Doc<TableName extends TableNames> = DocumentByName< DataModel, TableName >; export type DataModel = DataModelFromSchemaDefinition<typeof schema>;Doc<"messages">由schema.ts通过DataModelFromSchemaDefinition推导而来,也就是说:schema 中v.optional(v.string())定义的可选字段,最终决定了doc.optionalString的类型形态(string | undefined,属性可缺省)。这也解释了 4.2 节"读取时类型不含差异"的结论——文档类型中可选属性本来就是string | undefined,exactOptionalPropertyTypes的严格化主要体现在别处(对象字面量赋值、函数参数等场景)。
六、convex/tsconfig.json:函数运行时环境的独立配置
与根配置并列的 convex/tsconfig.json 描述的是Convex 函数的运行环境,用于对函数代码做类型检查。其注释明确了哪些是"Convex 必需项"、哪些可自由修改:
- 必需项(不可改动):
target: "ESNext"、lib: ["ES2023", "dom"]、forceConsistentCasingInFileNames、module: "ESNext"、isolatedModules、noEmit。 - 可修改项:
allowJs、strict、moduleResolution: "Bundler"、jsx: "react-jsx"、skipLibCheck、allowSyntheticDefaultImports。 - 包含范围:
include: ["./**/*"],但exclude: ["./_generated"]——自动生成代码不参与函数目录自身的类型检查。
这套"根配置 + convex 子配置"的双层结构是 Convex 工程的通用模式:根配置管整个 monorepo 包(这里是 strict + exactOptionalPropertyTypes 的严格验证),convex/子配置约束函数运行环境。示例刻意只在根配置中开启exactOptionalPropertyTypes,从而把验证焦点集中在 demo 目的上。
七、如何运行与验证
在 package.json 中可以看到两个脚本:
"scripts": { "dev": "convex dev", "build": "tsc" }- 类型检查:在示例目录执行
npm run build(等价于tsc),它会按根 tsconfig 校验全部源码。若convex/messages.ts中的@ts-expect-error断言被破坏(例如 Convex 类型生成已适配exactOptionalPropertyTypes,使某处不再报错),tsc会因"未使用的 expect-error 指令"而失败——这正是该示例作为回归验证的机制:一旦 Convex 修复了嵌套可选字段的建模问题,构建就会提示移除相应断言。 - 运行开发服务器:执行
npm run dev(等价于convex dev)可启动本地开发环境,CLI 会同步生成convex/_generated下的类型文件,并可在本地执行list查询观察行为。
需要说明的适用前提:该示例依赖convex包(workspace 引用)与 TypeScript 5.9.x;由于exactOptionalPropertyTypes对convex包自身声明文件不友好(见 2.3 节),skipLibCheck: true是当前可运行的必需条件。
八、实践要点小结
| 主题 | 结论 | 证据位置 |
|---|---|---|
| 可选字段读取 | exactOptionalPropertyTypes开启前后,读取文档可选字段的类型无差异(string \| undefined) | convex/messages.ts |
| 安全访问模式 | 用in操作符 + rest 解构收窄"属性缺失 / 存在"两种状态 | convex/messages.ts |
| 嵌套可选字段 | 当前生成类型对嵌套v.object内的可选字段建模不完整,存在@ts-expect-error待修复标记 | convex/messages.ts |
| 必须 skipLibCheck | convex 包的类型声明在exactOptionalPropertyTypes下无法通过检查 | tsconfig.json |
| 不开启 declaration | Convex 函数推断类型与declaration不兼容 | tsconfig.json |
| schema 侧写法 | 可选字段统一用v.optional(...)(表级与嵌套对象皆可) | convex/schema.ts |
简而言之,typescript-exact-optional-property-types是 convex-backend 仓库中一个"以极小代价验证激进严格类型配置"的样例:它把exactOptionalPropertyTypes与noUncheckedIndexedAccess、strict组合成一套可复制的 tsconfig 模板,用带断言的 query 函数把该选项在 Convex 文档类型上的行为差异固化下来,并如实标注了当前存在的兼容性边界(依赖库skipLibCheck、嵌套可选字段的生成类型缺陷)。对于希望在 Convex 工程中推进类型严格的开发者,这份配置与代码即为现成的起点与对照基准。
【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址: https://gitcode.com/gh_mirrors/co/convex-backend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考