资讯动态

tldraw 运行时验证库 @tldraw/validate 完全指南:从形状 Schema 校验到 API 查询守卫

发布时间:2026/9/10 10:37:34 来源:尧图企业网站定制
tldraw 运行时验证库 tldraw/validate 完全指南从形状 Schema 校验到 API 查询守卫【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw导读tldraw/validate是 tldraw 官方维护的运行时验证库负责对编辑器内部形状属性、文档记录Record以及 API 请求参数做运行时类型检查。本文以 packages/validate/README.md 为骨架结合仓库源码与测试用例系统讲解其核心 APIT命名空间下的各类验证器、ValidationError错误模型、validateUsingKnownGoodVersion性能优化机制以及它在 tldraw 的形状 Schema 与 Worker 请求校验中的真实落地方式。读完本文你将能独立使用该库为自己的数据模型编写类型安全的运行时校验并理解 tldraw 编辑器是如何保证画布数据一致性的。一、什么是 tldraw/validatetldraw/validate是 tldraw 生态中的运行时验证库其包描述为 A runtime validation library by tldraw当前版本为5.4.0见 packages/validate/package.json。它只依赖tldraw/utils一个工作区包属于 tldraw SDK 核心tldraw:sdk-core的组成部分。它解决的核心问题是TypeScript 的类型检查只存在于编译期而 tldraw 的文档数据形状、资产、记录会经历网络传输、本地存储、IndexedDB 读写、用户输入等多种不可信路径。为了让数据在运行时也保持结构正确tldraw 为每个数据模型都挂载了一套验证器。README 中明确给出了两个典型使用场景配合 tlschema 验证形状属性——保证形状对象上的属性类型一致验证 API 请求参数——例如检查查询字符串是否合法。此外README 还提到随包发布的DOCS.md文件位于 packages/validate/DOCS.md包含了更详细的 API 文档与使用示例本仓库的 packages/validate/src/lib/validation.ts约 2000 行是它的完整实现packages/validate/src/test 目录下还有 17 个针对性测试文件含模糊测试 validation.fuzz.test.ts。二、形状属性校验imageShapeProps 实战README 的第一个示例展示了 tldraw 如何用RecordPropsT声明图像形状的运行时属性 Schema。仓库中对应的真实实现位于 packages/tlschema/src/shapes/TLImageShape.tsexport const imageShapeProps: RecordPropsTLImageShape { w: T.nonZeroNumber, h: T.nonZeroNumber, playing: T.boolean, url: T.linkUrl, assetId: assetIdValidator.nullable(), crop: ImageShapeCrop.nullable(), flipX: T.boolean, flipY: T.boolean, altText: T.string, }这段声明传达了三个关键设计每个字段绑定一个专用验证器宽度/高度用T.nonZeroNumber拒绝 0 和负数URL 用T.linkUrl只允许http:、https:、mailto:协议天然防御javascript:注入资产 ID 用assetIdValidator.nullable()表示可以为 null图像未绑定资产时。嵌套验证器crop字段对应ImageShapeCrop后者本身也是用T.object组合出来的见 TLImageShape.tstopLeft和bottomRight用vecModelValidator校验坐标向量isCircle为可选布尔。这正是本库的组合式设计——验证器可以无限嵌套。校验与迁移协同在imageShapeMigrations中MakeUrlsValid版本直接用T.linkUrl.isValid(props.url)作为守卫来决定是否把非法 URL 重置为空字符串见 TLImageShape.ts。这展示了isValid不抛异常的布尔判断在数据迁移场景下的用法。RecordPropsT是一种把属性类型映射为验证器类型的映射工具它保证你写的验证器集合与 TypeScript 接口如TLImageShapeProps在编译期就保持一致——字段写漏、写错类型都会直接报 TS 错误。三、API 请求校验queryValidator 实战README 的第二个示例展示如何校验 API 查询参数const queryValidator T.object({ w: T.string.optional(), q: T.string.optional(), }) queryValidator.validate(request.query)这段代码在仓库中并非孤例而是真实运行在 Cloudflare Worker 中。tldraw 的图片缩放 Workerapps/dotcom/image-resize-worker/src/worker.ts就有一个几乎完全相同的queryValidator并通过parseRequestQuery(request, queryValidator)把不可信的 URL 查询字符串变成类型安全的对象随后用于构造缓存键见 worker.tsconst queryValidator T.object({ w: T.string.optional(), q: T.string.optional(), })这个场景体现了本库在边界防护上的价值任何来自外部的输入在进入业务逻辑之前先过一遍验证器。T.string.optional()表示该字段允许缺失值为undefined合法但一旦出现就必须是字符串。由于验证器会返回被校验后的原值referential equality这里query对象在成功路径上零拷贝、零额外分配。四、核心 API 全景T 命名空间整个库的公开入口是 packages/validate/src/index.ts它把lib/validation.ts中所有内容以T命名空间导出同时单独导出ArrayOfValidator、DictValidator、ObjectValidator、UnionValidator、Validator类与UnionValidatorConfig类型。4.1 三类基础接口ValidatorFnT(value: unknown) T的纯函数类型校验失败抛ValidationErrorValidatorUsingKnownGoodVersionFnIn, Out性能优化版本接收上一次已验证的已知良好值与新值可跳过未变化部分的校验ValidatableT核心接口约定validate(value)与可选的validateUsingKnownGoodVersion(knownGood, newValue)两个方法见 validation.ts。4.2 Validator 基类所有验证器都继承自ValidatorTvalidation.ts它提供五个常用方法方法作用典型使用validate(value)校验并返回原值失败抛ValidationError数据进入系统的唯一入口validateUsingKnownGoodVersion(known, newValue)引用相等直接返回旧值否则走增量校验协同编辑时的增量更新isValid(value)类型守卫返回布尔值不抛异常迁移逻辑、条件判断nullable()派生新验证器额外接受null可空字段optional()派生新验证器额外接受undefined可选字段refine(fn)派生新验证器允许转换值到新类型字符串 → 解析后的对象check(name?, fn)追加断言不改变类型可命名错误前缀范围检查、格式检查其中refine与check的实现细节值得注意validation.tsrefine内部把skipSameValueCheck置为true因为转换函数本就有意改变返回值需要跳过开发环境里必须返回同一引用的断言check在失败路径才拼接错误前缀成功路径零字符串分配带名字的重载会把错误包装成At x (check name): ...的形式便于在 Sentry 等平台分组。4.3 基础类型验证器T下提供了一套开箱即用的基础验证器validation.ts任意值T.anyany逃生舱、T.unknown保持unknown、T.unknownObject非 null 对象数组也通过、T.jsonValue合法 JSON 值、T.jsonDict()字符串键 JSON 值字典标量T.string、T.number、T.boolean、T.bigint、T.array只查数组结构数值家族均有 NaN/Infinity 的精确错误信息T.positiveNumber≥ 0注意命名含歧义源码明确accepts zeroT.nonZeroNumber 0图像宽高就用它T.nonZeroFiniteNumber非零但允许负数用于翻转等可为负的缩放因子T.unitInterval闭区间 [0, 1]适合透明度、百分比小数T.integer/T.positiveInteger/T.nonZeroInteger整数三兄弟语义同数字三兄弟组合T.arrayOf(item)、T.object(config)、T.dict(key, value)、T.union(key, config)、T.numberUnion(key, config)数字判别式internal、T.or(v1, v2)二选一、T.literal(x)、T.literalEnum(...values)等价于setEnum(new Set(...))、T.setEnum(set)、T.model(name, validator)、T.optional(v)、T.nullable(v)URL 三件套基于string.check实现T.linkUrlhttp:/https:/mailto:空串合法T.srcUrl额外允许data:与asset:asset:指 tldraw 本地 IndexedDB 对象存储引用源码注释见 validation.tsT.httpUrl仅http:/https:专属类型T.indexKeytldraw 排序键内部委托validateIndexKey。数值验证器的错误信息做到了按原因区分非数字、NaN、无穷、越界各有独立的ValidationError消息调试时一眼定位问题。4.4 复合验证器ObjectValidatorvalidation.ts默认拒绝未知属性Unexpected property可通过allowUnknownProperties()放开extend(extension)可基于现有 Schema 派生扩展版如基础用户 管理员字段。构造时会把 config 的键和验证器缓存为扁平数组每次校验走索引循环而非反复hasOwnProperty减少分配。ArrayOfValidatorvalidation.ts每个元素走同一验证器错误路径用rethrowPrefixed(i, err)注入下标At 0: ...额外提供nonEmpty()与lengthGreaterThan1()两个便捷约束。UnionValidatorvalidation.ts基于判别字段如type的路由验证器getMatchingSchema在热路径上避免分配{matchingSchema, variant}中间对象未知变体默认抛错也可用validateUnknownVariants(fn)自定义兜底逻辑。tldraw 的所有形状类型正是靠它按type字段分派校验的。DictValidatorvalidation.ts键值均校验for...in遍历避免Object.entries的数组分配。五、错误模型ValidationError 与路径前缀ValidationErrorvalidation.ts是理解本库调试体验的关键。它有两个公开字段rawMessage不含路径的原始错误信息path出错位置在数据结构中的路径数组如[users, 0, email]。super构造时会把路径格式化为At users.0.email: Expected valid URL这样的前缀。formatPathvalidation.ts还有两个细节支持(check name)这类括号段落的拼接形成At x (check finite): ...特意从路径中剔除id ...片段源码注释说明这是为了不让错误在 Sentry 等监控平台按 id 分散成海量分组。嵌套校验的错误通过rethrowPrefixedvalidation.ts逐层加上父级路径前缀该函数采用纯 try/catch而非回调包装保证成功路径不分配闭包。T.model(name, validator)validation.ts在此基础上再包一层模型名校验失败时错误变成At shape.y: Expected a number, got NaN。测试 model.test.ts 完整验证了这一行为——包括对NaN、枚举外值两类错误消息的断言。六、性能机制validateUsingKnownGoodVersion 增量校验tldraw 的编辑器在协同编辑、撤销重做等场景下会高频更新文档数据。Validator.validate在开发环境还会额外断言验证器必须返回传入的同一引用validation.ts这为增量校验奠定了基础。validateUsingKnownGoodVersion(knownGood, newValue)的核心策略validation.ts引用相等快速路径Object.is(knownGoodValue, newValue)直接返回旧值零校验否则若有自定义的validateUsingKnownGoodVersionFn委托给它兜底退化为完整校验。各复合验证器在此基础上实现了字段级增量比较ObjectValidator逐字段Object.is比较新旧值只有变化的字段才重新校验未知属性是否允许时还会额外检测属性删除/新增避免返回陈旧的 known-good 对象validation.tsArrayOfValidator先比长度再对每个元素做引用比较未变化元素直接跳过validation.tsDictValidator按键比较新旧值只在计数可能变化时才遍历旧对象validation.tsUnionValidator判别字段未变才走增量路径判别字段一旦变化如形状类型从circle变square立即退回完整校验validation.tsjsonValue对数组与普通对象递归复用该机制validation.ts。代码中多处出现// sneaky quick check here to avoid the prefix validator overhead这类注释体现的是同一个思路在热路径上尽量用Object.is引用比较代替完整校验让数据没变的成本趋近于零。formatPath、rethrowPrefixed、check等实现也都刻意避免在成功路径上做字符串拼接和闭包分配。七、如何在自己的项目中使用7.1 安装与引入tldraw/validate是 tldraw 工作区lerna.json、yarn.config.cjs中的独立包仓库内其他包通过tldraw/validate: workspace:*引用。在自己的项目里按常规方式安装该 npm 包后这样引入import { T, ValidationError } from tldraw/validate入口文件 packages/validate/src/index.ts 在导出时还会调用registerTldrawLibraryVersion注册库版本信息因此包内的副作用很小可以放心在浏览器、Node、Cloudflare Worker 等任意 JS 运行时使用README 的 API 校验示例就跑在 Worker 上。注意包的engines字段要求 Node 22.12.0package.json。7.2 一个完整的自定义模型示例import { T, ValidationError, type TypeOf } from tldraw/validate // 1. 组合出对象验证器 const userValidator T.model(User, T.object({ id: T.string, name: T.string.check(non-empty, (v) { if (v.trim().length 0) throw new ValidationError(Name must not be empty) }), email: T.linkUrl, age: T.positiveInteger.optional(), roles: T.arrayOf(T.literalEnum(admin, editor, viewer)).nonEmpty(), profile: T.object({ avatar: T.srcUrl.nullable(), bio: T.string }).allowUnknownProperties(), })) // 2. 从验证器反推 TS 类型 type User TypeOftypeof userValidator // 3. 守卫外部输入 function handleApiPayload(payload: unknown): User { try { return userValidator.validate(payload) // 错误会带完整路径前缀 } catch (err) { if (err instanceof ValidationError) { // err.path 例如 [roles, 0]err.rawMessage 是原始信息 console.error(Invalid payload at ${err.path.join(.)}: ${err.rawMessage}) } throw err } }这个例子几乎用到了本文提到的全部能力model命名、object组合、check命名断言、linkUrl/srcUrl的安全协议校验、positiveInteger/optional/nullable修饰、arrayOfliteralEnumnonEmpty组合、allowUnknownProperties宽松模式以及TypeOf的编译期类型推导。7.3 设计取舍小结拒绝未知属性是默认值T.object默认对未声明字段抛Unexpected property能第一时间发现前后端字段名不一致只有明确需要容忍扩展字段时才调用allowUnknownProperties()URL 校验自带安全语义面向用户的链接用linkUrl资源加载用srcUrl纯接口地址用httpUrl三者协议白名单不同可防止javascript:等危险协议进入 DOM数据边界一律过验证器参考 image-resize-worker 的做法把验证器放在 HTTP 入口而不是散落在业务代码中。八、相关资源与约定README 中列出的官方文档、参考文档、发布说明与 LLM 友好文档均托管在 tldraw.dev 官方站点外部链接此处不展开随包发布的 packages/validate/DOCS.md 提供了可离线阅读的详细 API 说明与更多使用示例。仓库内的 packages/validate/src/test 目录覆盖了数组、字典、枚举、错误、JSON、模型、可空可选、数字、对象、联合、原始类型、refine/check、URL 等全部主题的测试还包含一个模糊测试 validation.fuzz.test.ts是理解边界行为的绝佳参考packages/tlschema/src/shapes/TLImageShape.ts 则是验证器 类型 迁移三位一体的真实范例。tldraw/validate的设计哲学可以概括为组合式声明、路径化报错、引用级增量、热路径零分配。无论你是要在画布应用里维护形状数据的一致性还是要为后端/边缘 Worker 的请求入口加一道运行时防线这套 API 都能直接迁移复用。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价