
开发工具【免费下载链接】superstructA simple and composable way to validate data in JavaScript (and TypeScript).项目地址https://gitcode.com/gh_mirrors/su/superstruct点击查看免费下载Superstruct 内置了一组工具类型工厂utility type factories用于在已有 struct 的基础上进行组合与变换从而以最小成本复用、扩展和重构你的数据校验逻辑。本文围绕 docs/reference/utilities.md 展开逐一讲解assign、deprecated、dynamic、lazy、omit、partial、pick七个工具工厂的用法、底层实现原理与实战要点读完后你将掌握如何把多个对象 struct 合并、如何声明递归结构、如何按运行时输入动态切换校验规则以及如何从既有 struct 派生新 struct并能在 TypeScript 中正确地推演出对应类型。所有底层实现均可对照 src/structs/utilities.ts 与 src/structs/types.ts 阅读。工具类型工厂的作用在 Superstruct 中Struct是封装了校验逻辑的对象通过assert、is、validate、create、mask等入口对外提供服务见 src/struct.ts。基础类型工厂string()、number()、object()等解决了某个值长什么样的问题而工具类型工厂解决的是如何在既有 struct 之上派生出新的 struct组合已有 structassign把多个对象 struct 的属性合并进一个新的 struct变换已有 structomit、pick、partial分别对应删除、保留、可选化属性延迟与动态化lazy让递归结构可以定义dynamic让校验规则可以在运行时根据输入切换兼容过渡deprecated在保留校验的同时向使用者发出弃用警告。这七个工厂全部定义在 src/structs/utilities.ts并通过 src/index.ts 的export *统一对外导出。下面逐一深入。assign合并多个对象 struct 的属性assign通过混合多个既有对象 struct 的属性来创建一个新 struct其语义与 JavaScript 原生Object.assign类似import { assign, object, string } from superstruct const User assign( object({ id: string() }), object({ name: string() }) )合并后的 struct 同时接受两个来源的属性{ id: 1, name: Jane, }从 src/structs/utilities.ts 的实现可以看到三个关键行为只接受object与typestruct参数类型被约束为ObjectSchemaRecordstring, Struct见 src/utils.ts运行时通过Structs[0].type type判断第一个参数是type还是object。返回类型跟随第一个参数assign的返回 struct 类型与被 assign 进去的第一个 struct 保持一致。若第一个参数是typestruct则返回type(schema)否则返回object(schema)。这意味着若第一个参数是object合并结果会对未在 schema 中声明的未知属性报错若第一个参数是type未知属性则被允许这正是object与type的核心差异见 src/structs/types.ts 与 src/structs/types.ts。同名属性后者覆盖前者内部用Object.assign({}, ...schemas)把各 struct 的schema合并后面的 struct 若与前面有同名字段会覆盖前面的 schema 定义。实测用例 test/validation/assign/valid-object.ts 演示了同名覆盖语义assign(A, B)中A type({ a: string() })、B type({ a: number(), b: number() })最终a按B的number()校验{ a: 1, b: 2 }通过。类型层面的推导在 test/typings/assign.ts 中验证assign(object({ a: number() }), object({ b: string() }))可被assert断言为{ a: number; b: string }。deprecated校验的同时发出弃用警告deprecated用于标记某个已弃用的字段它先校验值是否匹配给定的 struct若值不是undefined则额外调用传入的log回调以便在运行时提醒使用者改用新 APIimport { deprecated, number, object, string } from superstruct const User object({ id: number(), full_name: string(), name: deprecated(string(), (value, ctx) { console.warn( ${ctx.path} is deprecated, but value was ${value}. Please use full_name instead. ) }), })当传入{ id: 1, name: Jane }时name字段的值不是undefined因此log回调会被触发并输出弃用警告ctx.path给出当前校验路径如[name]当字段缺失值为undefined时则静默通过。底层实现见 src/structs/utilities.tsreturn new Struct({ ...struct, refiner: (value, ctx) value undefined || struct.refiner(value, ctx), validator(value, ctx) { if (value undefined) { return true } else { log(value, ctx) return struct.validator(value, ctx) } }, })值得注意的细节它是改造既有 struct而非独立类型因此可与任意 struct 组合如deprecated(number(), () {})见 test/validation/deprecated/valid.ts当值合法但非undefined时log会在校验通过前被调用因此警告与校验错误是正交的两件事——值错误时同样会触发警告并报告校验失败ctx即Context包含branch校验分支、path当前路径与可选的mask标志定义见 src/struct.ts。在迁移场景中deprecated是渐进式改造的利器老字段仍可解析但使用时会收到明确的替代提示配合 docs/reference/errors.md 中的错误处理机制可以平滑过渡到新 schema。dynamic运行时按输入切换校验规则dynamic创建一个校验逻辑可以在运行时动态变化的 struct。回调接收(value, ctx)并根据当前输入返回用于继续校验的 structimport { dynamic, object, string, number } from superstruct const User object({ kind: string(), name: string() }) const Bot object({ kind: string(), power: number() }) const Entity dynamic((value) { return value.kind user ? User : Bot })dynamic的底层实现src/structs/utilities.ts将entries、validator、coercer、refiner四个阶段全部委托给回调返回的 struct因此dynamic不是简单替换校验函数而是完整转发整个校验管线return new Struct({ type: dynamic, schema: null, *entries(value, ctx) { const struct fn(value, ctx) yield* struct.entries(value, ctx) }, validator(value, ctx) { const struct fn(value, ctx) return struct.validator(value, ctx) }, coercer(value, ctx) { const struct fn(value, ctx) return struct.coercer(value, ctx) }, refiner(value, ctx) { const struct fn(value, ctx) return struct.refiner(value, ctx) }, })也就是说目标 struct 的强制类型转换coercion、条目遍历、基础校验与细化校验都会被完整继承。这意味着dynamic常用于按判别字段选择结构如value.kind决定走User还是Bot的校验按环境或配置切换规则例如根据运行模式选择宽松或严格的 struct在回调中先做断言再分发参考 test/validation/dynamic/valid-reference.ts回调里先用assert(entity, Entity)验证判别字段本身再返回映射表中对应的 struct实现引用查找式分发。由于回调可能被多次调用每个校验阶段各一次对性能敏感的场景应尽量让回调保持轻量或参考该测试用例用查找表预存 struct 引用。lazy定义自引用与递归结构lazy接收一个返回 struct 的函数校验时才会求值从而绕开 JavaScript 的循环引用问题用于定义递归数据结构import { lazy, array, number, object } from superstruct const Node object({ id: number(), children: lazy(() array(Node)), })底层实现src/structs/utilities.ts用闭包缓存了求值结果首次校验时执行fn()并缓存到struct ?? fn()此后每次校验复用同一个 struct避免重复构造export function lazyT(fn: () StructT, any): StructT, null { let struct: StructT, any | undefined return new Struct({ type: lazy, schema: null, *entries(value, ctx) { struct ?? fn() yield* struct.entries(value, ctx) }, validator(value, ctx) { struct ?? fn() return struct.validator(value, ctx) }, // coercer / refiner 同理 }) }注意lazy与dynamic的差别dynamic每次校验都根据value重新选择 structlazy只求值一次并缓存适合结构固定但定义上存在循环引用的场景如树、链表、嵌套评论等。 需要注意TypeScript 无法自动推断这种递归结构的类型因此lazy的返回类型StructT, null需要你手动传入泛型参数例如lazyArrayNode(() array(Node))否则类型会被推断为unknown。lazy同样会完整转发四个校验阶段因此与细化refinement结合也完全可行——test/validation/lazy/with-refiners.ts 中lazy(() nonempty(string()))能正常报告refinement: nonempty的失败信息说明包装后的 struct 的 refiner 被原样保留。omit从 struct 中排除指定属性omit基于既有object或typestruct 创建新 struct但排除指定属性对应 TypeScript 的Omit工具类型import { omit, object, number, string } from superstruct const User object({ id: number(), name: string(), }) const PublicUser omit(User, [name])实现src/structs/utilities.ts先浅拷贝原 schema再逐键删除最后按原 struct 的类型重新构造const { schema } struct const subschema: any { ...schema } for (const key of keys) { delete subschema[key] } switch (struct.type) { case type: return type(subschema) default: return object(subschema) }关键点只影响顶层属性omit基于struct.schema即顶层 schema操作不会递归影响嵌套 struct保留原 struct 的严格性源 struct 是objectomit结果仍是object未知属性报错源 struct 是type结果仍是type未知属性放行。测试 test/validation/omit/valid.ts 验证了omit(object(...), [age])后{ name: john }正常通过。典型场景是从全量实体派生出对外暴露的 DTO数据传输对象例如隐藏内部字段后再返回给前端。partial将全部属性变为可选partial基于既有object或typestruct 创建新 struct将其所有属性都变为可选允许undefined对应 TypeScript 的Partialimport { partial, object, number, string } from superstruct const User object({ id: number(), name: string(), }) const PartialUser partial(User)以下三种输入都能通过校验{ id: 1, name: Jane } { id: 1 } { name: Jane }实现src/structs/utilities.ts对 schema 中的每个字段包一层optional(...)const isStruct struct instanceof Struct const schema: any isStruct ? { ...struct.schema } : { ...struct } for (const key in schema) { schema[key] optional(schema[key]) } if (isStruct struct.type type) { return type(schema) } return object(schema)值得注意的两点partial也接受裸 schema 对象参数可以是StructObjectTypeS, S也可以是S即{ id: number() }这种字面量isStruct判断后统一处理。测试 test/validation/partial/valid-partial.ts 正是以裸 schema 调用partial({ name: string(), age: number() })。空对象也能通过partial后每个字段都可选因此{}也是合法输入这适合编辑表单只提交改动字段之类的场景。pick只保留指定属性pick与omit相反基于既有object或typestruct 只保留指定属性对应 TypeScript 的Pickimport { pick, object, number, string } from superstruct const User object({ id: number(), name: string(), }) const UserId pick(User, [id])实现src/structs/utilities.ts从原 schema 中按 keys 挑出字段构造新 schema并同样保留源 struct 的object/type属性const { schema } struct const subschema: any {} for (const key of keys) { subschema[key] schema[key] } switch (struct.type) { case type: return type(subschema) default: return object(subschema) }与omit相同pick也只作用于顶层 schema。测试 test/validation/pick/valid-type.ts 验证了从typestruct pick 出的结果仍是type{ name: john, unknownProperty: true }能带着未知属性通过校验。组合使用真实场景示例工具工厂的真正价值在于可以自由组合。例如定义一个创建用户的输入模型基础字段全量校验但更新时只允许提交部分字段并隐藏内部字段import { assign, dynamic, lazy, object, omit, partial, pick, string, number } from superstruct const BaseUser object({ id: number(), name: string(), email: string(), password: string(), }) // 对外 API排除内部字段 const PublicUser omit(BaseUser, [password]) // 更新接口所有字段可选 const UpdateUser partial(pick(BaseUser, [name, email, password])) // 递归结构组织树 const Organization object({ name: string(), children: lazy(() array(Organization)), }) // 运行时分发按 type 字段选择不同 schema const Entity dynamic((value) { return value.type organization ? Organization : PublicUser })再结合 docs/reference/core.md 中的assert/is/validate/create/mask入口使用即可。需要提醒的是这些工具只改变校验结构不会修改原始 struct 对象——assign、omit、partial、pick都会基于schema浅拷贝构造新的 struct因此可以放心复用基础定义。类型推导与工程注意点各工厂的返回类型在声明层面已做了映射assign返回ObjectTypeAssignA, Bomit返回ObjectTypeOmitS, Kpartial返回ObjectTypePartialObjectSchemaSpick返回ObjectTypePickS, K相关类型别名Assign、Omit、Pick、PartialObjectSchema集中在 src/utils.ts。因此合并、删除、可选化之后TypeScript 的类型收窄是自动的无需手写接口。lazy是唯一需要手动标注泛型的工具因为递归类型无法自动推断其余工厂均可直接依赖推断。dynamic的回调在每个校验阶段都会被执行若回调内部依赖外部状态需保证各阶段返回的 struct 一致避免校验与强制转换阶段结构漂移。若需要自定义全新的基础校验类型可参考define同文件 src/structs/utilities.ts旧版名为struct的辅助函数已弃用并重命名为define调用时会打印迁移提示。工具类型工厂让 Superstruct 的 struct 不再是一次性定义而是可以像 TypeScript 的类型工具一样被持续组合、派生与演进。结合 docs/reference/types.md 掌握基础类型再配合本文的工具工厂即可覆盖绝大多数数据建模与校验场景。赞分享开发工具【免费下载链接】superstructA simple and composable way to validate data in JavaScript (and TypeScript).项目地址https://gitcode.com/gh_mirrors/su/superstruct点击查看免费下载相关推荐Folly DynamicConverter 完全指南dynamic 与强类型 C 对象的双向转换Folly DynamicConverter 完全指南dynamic 与强类型 C 对象的双向转换 folly::dynamic 是 Folly 提供的运后端并发编程异步编程XTREME 是什么谷歌 40 种语言 9 大任务跨语言基准测试完全解读XTREME 是什么谷歌 40 种语言 9 大任务跨语言基准测试完全解读 XTREMECross lingual TRansfer Evaluation oApache Arrow C 数据类型体系全解析DataType、类型工厂与 Schema 结构Apache Arrow C 数据类型体系全解析DataType、类型工厂与 Schema 结构 本指南以 docs/source/cpp/api/dat数据工程大数据序列化数据分析上一篇Open SWE技术架构演进从v1到v2的重大改进与新特性下一篇Synology_HDD_db兼容性矩阵支持的群晖机型全列表创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考