ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Superstruct 2.0 运行时数据验证实战:用可组合的 Schema 守护 JavaScript 与 TypeScript 数据边界

Superstruct 2.0 运行时数据验证实战:用可组合的 Schema 守护 JavaScript 与 TypeScript 数据边界 开发工具【免费下载链接】superstructA simple and composable way to validate data in JavaScript (and TypeScript).项目地址https://gitcode.com/gh_mirrors/su/superstruct点击查看免费下载Superstruct是一个简单且可组合的 JavaScript及 TypeScript运行时数据验证库你用接近 TypeScript 类型注解的语法声明数据结构称为struct再通过assert、is、validate等函数在运行时校验任意输入数据。本文以仓库根目录 Readme.md 为骨架结合 src/struct.ts 等源码实现与 examples 下的示例完整讲解如何定义校验规则、内置类型、自定义类型、数据强制转换coercion、精化规则refinement、错误处理以及 TypeScript 类型推导读完即可在 REST/GraphQL API、表单校验、内部数据结构守护等场景落地一套以单一 schema 为单一事实来源的验证体系。一、核心概念用 struct 描述数据的形状Superstruct 的核心思想是先定义数据长什么样再拿它去校验未知输入。定义校验规则的产物是一个Struct对象它封装了某一种类型值的完整校验逻辑类型、schema、coercer、validator、refiner、entries 六个核心字段见 src/struct.ts。它的类型注解 API 设计灵感来自 TypeScript、Flow、Go 的 struct 语法和 GraphQL schema因此对熟悉这些语言的开发者来说上手成本极低。与静态类型不同的是Superstruct 专为运行时校验设计校验失败时抛出或返回包含丰富细节的运行时错误非常适合 REST/GraphQL API 接收任意外部输入、或对内部数据结构做运行时守护等场景。一个最基础的使用示例对应 examples/basic-validation.jsimport { assert, object, number, string, array, optional, boolean } from superstruct // 1. 定义 struct描述期望的数据形状 const User object({ id: number(), name: string(), email: string(), age: number(), departments: array(string()), is_admin: optional(boolean()), }) // 2. 待校验的数据注意is_admin 缺失但它是 optional 的所以合法 const data { id: 1, name: Jane Smith, email: janeexample.com, age: 42, departments: [engineering, product], } // 3. 校验数据合法则不抛错非法则抛出 StructError assert(data, User)二、五个核心验证 APIassert / is / validate / create / mask所有顶层验证函数都定义在 src/struct.ts它们共享同一个底层执行引擎runsrc/utils.ts只是对外暴露的返回形式不同函数行为失败时的表现是否执行 coercionassert(value, struct, message?)校验并断言值为 struct 类型抛出StructError否is(value, struct)返回布尔值同时充当 TypeScript 类型守卫返回false否validate(value, struct, options?)返回二元组结果返回[StructError, undefined]可控{ coerce: true }create(value, struct, message?)先强制转换再校验返回结果值抛出StructError是始终mask(value, struct, message?)转换 校验并剔除 schema 未声明的多余属性抛出StructError是始终且启用 mask其中assert、is、validate是最常用的三种校验方式对应 Readme 中的三种错误处理策略throw抛错boolean 判断不抛错tuple 返回不抛错且拿到错误对象。用官方文档的原话This will throw an error when the data is invalid. If youd rather not throw, you can useis()orvalidate().validate的返回值签名是[StructError, undefined] | [undefined, T]src/struct.ts配合可选的coerce、mask、message三个选项Struct实例上也直接挂载了同名的五个方法可直接User.validate(data)调用。底层run的执行顺序是先 coerce若开启→ 再执行 validator → 沿 entries 递归校验嵌套值 → 最后执行 refiner全程使用生成器generator惰性求值因此校验在遇到第一个失败时即可提前结束兼顾性能源码注释明确写着Validation logic is design to exit early for maximum performance见 src/error.ts。三、内置类型全集覆盖所有常见 JavaScript 数据类型Superstruct 内置了几乎全部常见 JS 数据类型的校验器全部定义在 src/structs/types.ts类型函数校验内容any()任意值都通过类型放宽为anyunknown()任意值都通过但类型保持unknown比any()更安全string()/number()/boolean()/bigint()基本类型number()额外排除NaNbigint()校验typeof bigintinteger()必须是整数Number.isIntegerdate()必须是Date实例且排除无效日期isNaN(value.getTime())的 Date 对象也判失败func()必须是函数array(Element)必须是数组且逐元素按Element校验不传参数时只校验是数组不遍历元素性能敏感场景推荐tuple([A, B, ...])定长元组第 i 位元素按第 i 个 struct 校验多余元素按never()判失败object(schema)非数组对象属性逐一按 schema 校验未声明的额外属性会判失败never类型type(schema)同object但允许未声明的额外属性接近 TypeScript 结构化类型语义record(Key, Value)键与值分别校验的字典结构类似 TS 的Recordmap(Key, Value)/set(Element)分别校验Map/Set的键值或元素literal(constant)用精确匹配某个字符串/数字/布尔值enums([...values])值必须是给定列表之一可通过struct.schema访问候选值定义instance(Class)必须是某类的实例regexp()必须是RegExp对象注意不做字符串匹配测试匹配要用下面的pattern()精化nullable(struct)/optional(struct)包装现有 struct分别额外允许null/undefinedunion([A, B, ...])/intersection([A, B, ...])联合类型任一匹配即可/ 交叉类型全部匹配never()任何值都失败用于表示不可能出现的值嵌套、数组、对象可以自由组合object({ author: object({ id: number() }) })、array(string())、object({ tags: array(enums([news, features])) })等写法在 Readme 示例和 src/structs/types.ts 中均有体现。从源码看object()的 entries 生成逻辑会先遍历 schema 中已声明属性再对剩余未知属性套用never()使其失败src/structs/types.ts——这正是对象禁止未知属性这一默认行为的实现原理。四、自定义类型define 一行搞定专属校验器Readme 强调Superstruct ships with validators for all the common JavaScript data types, and you can define custom ones too. 自定义类型使用define(name, validator)实现见 src/structs/utilities.tsvalidator 返回boolean、错误字符串或失败对象即可import { is, define, object, string } from superstruct import isUuid from is-uuid import isEmail from is-email const Email define(Email, isEmail) const Uuid define(Uuid, isUuid.v4) const User object({ id: Uuid, email: Email, name: string(), }) const data { id: c8d63140-a1f7-45e0-bfc6-df72973fea86, email: janeexample.com, name: Jane, } if (is(data, User)) { // 进入该代码块即代表 data 已通过全部校验 }自定义类型与内置类型地位完全平等可以自由嵌套在object、array、optional等任意组合中。这也是 Superstruct 的核心设计之一在应用的单一位置定义整套专属类型作为数据的单一事实来源。提示早期版本中定义自定义类型的助手叫struct()现已更名为define()旧名称会打印弃用警告见 src/structs/utilities.ts。五、数据强制转换Coercion校验前先修正数据Superstruct 支持在验证前对数据做强制转换coercion例如混入默认值、裁剪字符串等。关键前提必须使用create()或validate(..., { coerce: true })、mask()才会触发 coercion——直接用assert()或is()不会执行任何转换src/structs/coercions.ts 源码注释明确说明了这一点。内置三个转换助手全部定义在 src/structs/coercions.tsdefaulted(struct, fallback, options?)把undefined替换为默认值。fallback 可以是值或函数函数每次求值可用于自增 id非严格模式下options.strict默认 false若原始值与 fallback 都是纯对象还会递归合并缺失属性。trimmed(struct)仅当输入是字符串时对其执行trim()底层用coerce(struct, string(), (x) x.trim())。coerce(struct, condition, coercer)底层通用原语——当输入满足condition这个 struct 时才执行coercer函数转换。Readme 中的默认值示例原文基于() i自增 id这里给出等价写法import { create, object, number, string, defaulted } from superstruct const User object({ id: defaulted(number(), () 1), name: string(), }) const data { name: Jane } // create 会先应用默认值再执行校验并返回结果 const user create(data, User) // { // id: 1, // name: Jane, // }mask()是另一个值得一提的转换场景它除执行 coercion 外还会把objectstruct 中未在 schema 声明的属性从结果中删除。这一行为在 src/structs/types.ts 中实现当ctx.mask为真时coercer 会遍历并删除多余键——适合白名单式清洗 API 入参。union结构同样支持带 mask 的逐分支 coercion见 src/structs/types.ts。六、精化规则Refinement在类型之上叠加业务约束类型校验只能回答值的种类对不对业务约束长度、范围、格式则由精化完成。refine(struct, name, refiner)是通用原语src/structs/refinements.ts它保证 refiner 拿到的已经是通过该 struct 原有校验的值因此可以安全地对值本身做进一步判断。内置精化规则一览函数适用类型约束min(struct, threshold, { exclusive })number / Date大于等于默认或大于exclusive阈值max(struct, threshold, { exclusive })number / Date小于等于默认或小于exclusive阈值size(struct, min, max min)string / array / number / Date / Map / Set长度/数值/大小介于 min 与 max 之间省略 max 即精确匹配pattern(struct, regexp)string匹配正则empty(struct)string / array / Map / Set大小为 0nonempty(struct)string / array / Map / Set大小大于 0自定义精化示例如邮箱必须是 example.com 域名import { refine, string } from superstruct const Email refine(string(), email, (value) /^[^]example\.com$/.test(value) || Expected a valid example.com email address )refine的实现会把失败信息打上refinement名称标记src/structs/refinements.ts该名称会出现在错误对象的refinement字段中便于上层区分类型错误与业务规则错误。仓库的 docs/reference/refinements.md 与 src/structs/refinements.ts 可进一步查阅全部精化的精确语义refine还支持嵌套叠加即对已精化的 struct 再次refine对应的测试见 test/validation/refine/invalid-multiple-refinements.ts。七、组合与工具函数从组件拼出复杂结构Readme 明确提到 Superstruct 支持composing structs inside each other。除了把 struct 当作值嵌套进object/array之外src/structs/utilities.ts 还提供了一组面向对象结构的组合工具语义与 TypeScript 工具类型一一对应assign(A, B, ...)合并多个 object/type struct 的属性类似Object.assign返回类型以第一个参数的 struct 类型为准。pick(struct, keys)只保留指定属性类似 TS 的Pick。omit(struct, keys)剔除指定属性类似 TS 的Omit。partial(struct)把所有属性变为可选类似 TS 的Partial。union([...])/intersection([...])类型层面的或/与组合。lazy(() struct)惰性求值首次校验时才调用回调用于解决递归/自引用数据结构的循环定义问题如树形结构。dynamic((value, ctx) struct)根据当前值动态决定校验规则。deprecated(struct, log)允许值为undefined不再传该字段但传入时记录日志——适合渐进式淘汰旧字段。这些工具与可组合的接口设计原则相呼应把反复出现的数据片段拆成组件再拼装出更复杂的对象。仓库的 examples/composing-structs.js 给出了组合 struct 的直观演示。八、错误处理读懂 StructError自定义应用级错误Superstruct 默认抛出便于开发者定位问题的错误。例如对下面的 structconst User object({ id: number(), name: string(), email: string(), })传入非法数据时const data { id: 1, name: Alex, email: false } assert(data, User) // StructError: At path: email -- Expected a string, but received: falseStructError继承自原生TypeErrorsrc/error.ts抛出/返回的错误对象上带有完整定位信息对应Failure结构src/error.tsStructError { value: false, // 出问题的值 key: email, // 出问题的键 type: string, // 期望的类型 refinement: undefined, // 若为精化失败这里是精化名称 path: [email], // 从根到出问题位置的路径 branch: [{ id: 1, name: Alex, email: false }, false], // 沿路径的每一层值 failures: [Function] // 惰性生成全部失败的迭代器 }你还可以传入自定义消息assert(data, User, The user is invalid!)此时原消息会被保留在error.cause中对应仓库 docs/guides/05-handling-errors.md 的说明与 src/utils.ts 的实现。自定义应用级错误有了上述结构化信息捕获后即可转换成符合自身 API 规范的错误。例如 REST API 创建用户时示例取自 docs/guides/05-handling-errors.mdrouter.post(/users, ({ request, response }) { const data request.body try { assert(data, User) } catch (e) { const { key, value, type } e if (value undefined) { const error new Error(user_${key}_required) error.attribute key throw error } else if (type never) { const error new Error(user_attribute_unknown) error.attribute key throw error } else { const error new Error(user_${key}_invalid) error.attribute key error.value value throw error } } })这样下游拿到的错误码是统一的user_email_invalid、user_email_required、user_name_invalid……判断type never即可识别未声明的未知属性这正是object对多余属性的处理方式与 src/structs/types.ts 的实现一一对应。收集全部失败error.failures()默认情况下assert只抛出校验中遇到的第一个失败以保证最佳性能run引擎的惰性生成器设计src/utils.ts。如果需要一次性拿到所有失败信息可以调用error.failures()生成器try { assert(data, Struct) } catch (error) { for (const failure of error.failures()) { // 逐个处理每个失败 } }failures是按需求值的Superstruct 在迭代它之前并不知道第一个失败之后的其余失败这种惰性设计在失败场景下能显著提升性能src/error.ts。错误相关的完整参考见 docs/reference/errors.md。九、TypeScript 集成校验即类型收窄Readme 重点强调了 Superstruct 的 TypeScript 体验校验通过后数据自动获得完整类型。这是因为is被声明为类型守卫value is T、assert被声明为断言函数asserts value is Tsrc/struct.tsimport { is, object, number, string } from superstruct const User object({ id: number(), name: string(), }) const data: unknown { ... } if (is(data, User)) { // 此处 TypeScript 已知 data 的形状可安全访问 data.id、data.name }配合类型工具InferT从 struct 提取类型与DescribeT从 TS 类型生成 struct可以实现一个定义、两处受益用Infertypeof User把 struct 反推成 TypeScript 类型避免手动重复声明接口用DescribeT从既有 TS 接口生成 struct校验规则与静态类型保持同步。object结构对optional/defaulted的处理还做了类型层面的归一化允许undefined的属性会被自动转换为可选属性Optionalize见 src/utils.ts因此推导出的类型与手写接口观感一致。类型相关深度内容见 docs/guides/06-using-typescript.md 与 docs/reference/typescript.md。十、为什么是 Superstruct设计动机与原则Readme 的 Why? 一节梳理了作者对既有验证库的观察也解释了本项目存在的理由很多库不暴露详细错误只返回布尔值或纯字符串错误难以定制对终端用户友好的错误自定义类型很难内置了 email、URL、UUID 等类型却无法知道其检查逻辑定义新类型的 API 复杂不鼓励单一事实来源同样的数据形状被反复重新定义散落在整个代码库不真正抛错迫使调用方到处手动包装与现代 JavaScript 的throw风格相悖与框架强耦合绑定 Express 等框架产生无法复用的零散代码依赖 JSON Schema对大多数场景而言复杂度远超实际收益。对应地Superstruct 提出五条设计原则Readme Principles 一节Customizable types可定制类型——在单一位置定义整套应用专属类型完全掌控校验需求Unopinionated defaults默认不设限——只内置原生 JS 类型其余全部可定制不与核心的决策打架Composable interfaces可组合接口——把重复数据片段拆成组件再拼装Useful errors有用的错误——错误携带全部信息易于转换为应用专属错误Familiar API熟悉的 API——语法受 TypeScript、Flow、Go、GraphQL 启发学习曲线平缓。十一、示例、文档与工程信息仓库在 examples 目录下提供了 9 个可直接运行的演示脚本覆盖绝大多数常见模式建议按需取用Basic ValidationCustom TypesDefault ValuesOptional ValuesComposing StructsThrowing ErrorsReturning ErrorsTesting ValuesCustom Errors深入的指南与 API 参考同样收录在仓库中原 Readme 指向的是外部文档站链接这里给出仓库内的等价路径入门指南01-getting-started → 02-validating-data → 03-coercing-data → 04-refining-validation → 05-handling-errors → 06-using-typescriptAPI 参考core、types、refinements、coercions、utilities、errors、typescript文档总览可先看 docs/readme.md工程信息以 package.json 为准当前仓库版本为2.0.2type: module提供dist/index.cjsCommonJS、dist/index.mjsESM与dist/index.d.ts类型声明三种产物sideEffects: false运行环境要求 Node.js 14.0.0。测试覆盖在 test 目录下按 API 与类型分门别类例如 test/api/assert.test.ts、test/api/validate.test.ts以及按类型组织的 test/validation 用例可作为理解各函数边界行为的第一手资料。项目采用 MIT 许可License.md。小结Superstruct 的核心价值在于用接近静态类型注解的语法声明数据契约让契约同时服务于运行时校验与TypeScript 类型推导并通过assert/is/validate三种错误策略、create/mask的转换能力、refine的业务约束、以及assign/pick/omit/lazy等组合工具覆盖从简单对象到递归树结构的全部校验场景。无论你是要在 API 入口拦截非法输入、清洗外部数据还是想为内部数据结构加一道运行时保险都可以从 docs/guides/01-getting-started.md 开始配合本文的源码路径逐步深入。赞分享开发工具【免费下载链接】superstructA simple and composable way to validate data in JavaScript (and TypeScript).项目地址https://gitcode.com/gh_mirrors/su/superstruct点击查看免费下载相关推荐Front-End-Checklist 运行时数据校验实战用 Zod/Valibot 在信任边界守护 TypeScript 类型安全Front End Checklist 运行时数据校验实战用 Zod/Valibot 在信任边界守护 TypeScript 类型安全 运行时校验RuntimUnifoLM-WMA-0安全部署指南确保机器人操作稳定可靠的7个关键点UnifoLM WMA 0安全部署指南确保机器人操作稳定可靠的7个关键点 想要让你的Unitree机器人稳定运行UnifoLM WMA 0世界模型框架吗人工智能具身智能机器人计算机视觉媒体生成预训练Superstruct与TypeScript完美结合类型安全的数据验证方案Superstruct与TypeScript完美结合类型安全的数据验证方案 你是否还在为JavaScript项目中的数据验证与类型安全问题烦恼是否希望找到一开发工具上一篇Vega 直方图Histogram实战指南基于 bin 变换的分箱统计与交互式可视化下一篇SO-ARM100开源模块化机器人手臂系统的全栈技术解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表