ARTICLE DETAIL

资讯详情

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

Superstruct 类型系统全解析:25 个内置类型工厂与自定义类型的实战指南

Superstruct 类型系统全解析:25 个内置类型工厂与自定义类型的实战指南 开发工具【免费下载链接】superstructA simple and composable way to validate data in JavaScript (and TypeScript).项目地址https://gitcode.com/gh_mirrors/su/superstruct点击查看免费下载Superstruct 提供了一套用于 JavaScript及 TypeScript数据校验的简单、可组合的 API。本文以官方参考文档 docs/reference/types.md 为骨架逐一剖析其暴露的 25 个内置类型工厂函数any、array、bigint、boolean、date、enums、func、instance、integer、intersection、literal、map、never、number、nullable、object、optional、record、regexp、set、string、tuple、type、union、unknown并结合 src/structs/types.ts 的源码实现与 test/validation 目录下的测试用例说明每个类型的校验逻辑、边界行为与组合用法。读完本文你将能够熟练选用合适的类型工厂构建结构化校验器并通过define定义属于自己的自定义类型。一、类型工厂背后的统一模型在深入每个类型之前先理解一个关键事实所有类型工厂函数最终都返回一个Struct实例。Struct类封装了四段可组合的逻辑validator校验值是否属于该类型返回true/false、错误消息字符串或StructFailure对象见 struct.ts 中Result的定义refiner在类型已成立的基础上做进一步约束如min、pattern等 refinement见 docs/reference/refinements.mdcoercer在校验前把输入值转换为目标形态如数组浅拷贝、Map/Set克隆entries以生成器方式产出嵌套子值及其对应子 struct让object、array等容器类型能够递归校验子属性。调用方通过assert、is、validate、create、mask五个核心函数触发这些逻辑详见 docs/reference/core.md。本文只关注Struct的“类型”维度——即validator与entries的行为。另外src/structs/utilities.ts 中的define(name, validator)是所有简单类型工厂的公共底层export function defineT(name: string, validator: Validator): StructT, null { return new Struct({ type: name, schema: null, validator }) }也就是说像string()、number()这些一行实现本质上是给一段校验函数起个名字。理解这一点后下面每个类型的行为都会非常直观。二、基础标量类型string()string()validstringstruct 校验值是否为字符串。源码实现src/structs/types.tsexport function string(): Structstring, null { return define(string, (value) { return ( typeof value string || Expected a string, but received: ${print(value)} ) }) }失败时返回形如Expected a string, but received: 42的错误消息。print工具负责把任意值渲染成可读的错误描述。number()number()0 3.14 42 Infinitynumberstruct 校验值是否为数字src/structs/types.tsexport function number(): Structnumber, null { return define(number, (value) { return ( (typeof value number !isNaN(value)) || Expected a number, but received: ${print(value)} ) }) }从源码可以得出两个精确的边界行为NaN会被拒绝——因为isNaN(NaN)为trueInfinity会被接受——isNaN(Infinity)为false且官方文档的合法值示例中明确列出了Infinity。bigint()bigint()0n 3n 4000030n BigInt(10n ^ 1000n)bigintstruct 校验值是否为 bigintsrc/structs/types.ts实现即typeof value bigint。对应测试见 test/validation/bigint/valid.ts 与 test/validation/bigint/invalid.ts。boolean()boolean()true falsebooleanstruct 只接受布尔值true和falsesrc/structs/types.ts实现为typeof value boolean。注意0、1、true等类布尔值并不会被隐式转换一律校验失败。integer()integer()-7 0 42integerstruct 校验值是否为整数src/structs/types.tsexport function integer(): Structnumber, null { return define(integer, (value) { return ( (typeof value number !isNaN(value) Number.isInteger(value)) || Expected an integer, but received: ${print(value)} ) }) }它在number()的基础上追加了Number.isInteger(value)条件因此小数如3.14会被拒绝测试用例见 test/validation/integer/invalid-decimal.ts。func()func()function () {}funcstruct 校验值是否为函数src/structs/types.ts实现为typeof value function。常用于鸭子类型场景只关心某个属性是可调用的而不关心其具体签名。literal()literal(42)42literalstruct 使用运算符强制值必须精确等于给定常量src/structs/types.tsexport function literalT(constant: T): any { const description print(constant) const t typeof constant return new Struct({ type: literal, schema: t string || t number || t boolean ? constant : null, validator(value) { return ( value constant || Expected the literal \${description}\, but received: ${print(value)} ) }, }) }它接受字符串、数字、布尔值等任意常量但注意对对象是引用比较——传入对象字面量时只有同一引用才能通过。enums()enums([Jane, John, Jack, Jill])Jane Johnenumsstruct 校验值是否为给定字面量集合中的一员src/structs/types.tsexport function enumsU extends string | number, T extends readonly U[](values: T): any { const schema: any {} const description values.map((v) print(v)).join() for (const key of values) { schema[key] key } return new Struct({ type: enums, schema, validator(value) { return ( values.includes(value as any) || Expected one of \${description}\, but received: ${print(value)} ) }, }) }两个实现细节值得注意schema 是可访问的源码注释说明创建后可读取struct.schema获取候选值集合形如{ Jane: Jane, John: John, ... }支持数字与字符串混合集合重载签名限定为string | number。对应测试见 test/validation/enums/valid.ts。三、时间与正则类型date()date()new Date()datestruct 校验值是否为 JavaScriptDate实例src/structs/types.tsexport function date(): StructDate, null { return define(date, (value) { return ( (value instanceof Date !isNaN(value.getTime())) || Expected a valid \Date\ object, but received: ${print(value)} ) }) }官方文档特别强调datestruct 不接受无效的Date对象。new Date(invalid)在技术上仍是Date实例但getTime()会返回NaN因此被拒绝——这避免了无效日期在后续逻辑中引发难以排查的运行时错误。测试见 test/validation/date/invalid.ts。regexp()regexp();/\d/ new RegExp()regexpstruct 校验值是否为RegExp对象src/structs/types.ts实现为value instanceof RegExp。官方文档有一个必须牢记的提醒它不会用正则去匹配值本身如果你要对字符串做正则匹配校验应该使用pattern()refinement见 docs/reference/refinements.md#patternimport { pattern, string } from superstruct const DigitString pattern(string(), /\d/)四、容器类型数组、元组与对象array()array(number()) array(object({ id: number() }))[1, 2, 3] [{ id: 1 }]arraystruct 校验值为数组且其元素必须匹配指定的元素类型src/structs/types.tsexport function arrayT extends Structany(Element?: T): any { return new Struct({ type: array, schema: Element, *entries(value) { if (Element Array.isArray(value)) { for (const [i, v] of value.entries()) { yield [i, v, Element] } } }, coercer(value) { return Array.isArray(value) ? value.slice() : value }, validator(value) { return Array.isArray(value) || Expected an array value, but received: ${print(value)} }, }) }源码注释明确了省略参数的行为array()不带元素 struct 时数组元素完全不会被遍历校验。文档建议在性能敏感场景下用array()而非array(any())可以避免无谓的逐元素迭代。另外coercer会执行value.slice()浅拷贝——配合create()/mask()使用时返回的是新数组而非原引用。tuple()tuple([string(), number(), boolean()])[a, 1, true]tuplestruct 校验值为固定长度的数组且每个位置的元素类型与声明一一对应src/structs/types.tsexport function tupleA extends AnyStruct, B extends AnyStruct[](Structs: [A, ...B]): any { const Never never() return new Struct({ type: tuple, schema: null, *entries(value) { if (Array.isArray(value)) { const length Math.max(Structs.length, value.length) for (let i 0; i length; i) { yield [i, value[i], Structs[i] || Never] } } }, ... }) }关键实现细节循环取max(声明长度, 实际长度)超出声明长度的多余元素会交给never()struct 校验从而必然失败。测试 test/validation/tuple/invalid-element-unknown.ts 印证了这一行为[A, 3, unknown]对tuple([string(), number()])校验时第三个元素以type: never的失败信息被拒绝。object()object({ id: number(), name: string(), }){ id: 1, name: Jane Smith, }objectstruct 校验值为普通对象且每个已声明属性都必须匹配对应类型src/structs/types.tsexport function objectS extends ObjectSchema(schema?: S): any { const knowns schema ? Object.keys(schema) : [] const Never never() return new Struct({ type: object, schema: schema ? schema : null, *entries(value) { if (schema isObject(value)) { const unknowns new Set(Object.keys(value)) for (const key of knowns) { unknowns.delete(key) yield [key, value[key], schema[key]] } for (const key of unknowns) { yield [key, value[key], Never] } } }, validator(value) { return isNonArrayObject(value) || Expected an object, but received: ${print(value)} }, ... }) }源码揭示了两个值得展开的行为多余属性必然失败所有未在 schema 中声明的属性都会被收集到unknowns集合并交给never()校验。测试 test/validation/object/invalid-property-unknown.ts 显示{ name: john, age: 42, unknown: true }中unknown属性产生type: never、path: [unknown]的失败。官方文档指出若不想对多余属性报错应改用type若想在宽容输入的同时丢弃多余属性则用maskmask触发时object的coercer会直接从拷贝对象中删除未声明属性见 src/structs/types.ts数组不是对象validator使用isNonArrayObject因此数组会被object()拒绝对应测试 test/validation/object/invalid-array.ts。type()type({ name: string(), walk: func(), }){ name: Jill, age: 37, race: human, walk: () {}, }typestruct 校验对象必须拥有声明的一组属性但对未声明的属性不做任何断言src/structs/types.tsexport function typeS extends ObjectSchema(schema: S): StructObjectTypeS, S { const keys Object.keys(schema) return new Struct({ type: type, schema, *entries(value) { if (isObject(value)) { for (const k of keys) { yield [k, value[k], schema[k]] } } }, ... }) }对比object的实现可见本质差异type的entries只迭代keys已声明属性从不生成never()来检查未知属性。这在语义上类似 TypeScript 的结构类型structural typing——只要对象具备所需的功能点即可通过不要求键集合完全相等。源码注释还明确指出当mask()作用于typestruct 时未知属性也不会被移除——type是向 core 传递对象可能带任意额外属性这一信号的机制。测试见 test/validation/type/valid.ts。record()record(string(), number()){ a: 1, b: 2, }recordstruct 校验对象的键和值分别匹配指定类型但不强制任何具体的键集合src/structs/types.tsexport function recordK extends string, V(Key: StructK, Value: StructV): any { return new Struct({ type: record, schema: null, *entries(value) { if (isObject(value)) { for (const k in value) { const v value[k] yield [k, k, Key] yield [k, v, Value] } } }, validator(value) { return isNonArrayObject(value) || Expected an object, but received: ${print(value)} }, ... }) }record的行为类似 TypeScript 的RecordK, V工具类型每个键作为字符串和每个值都要分别通过Key与Valuestruct 的校验。任何键不满足Key或任何值不满足Value都会产生对应路径的失败。map()与set()map(string(), number())new Map([ [a, 1], [b, 2], ])set(string())new Set([a, b, c])mapstruct 校验值为Map对象且其键、值分别匹配指定类型src/structs/types.tssetstruct 校验值为Set实例且元素匹配指定类型src/structs/types.ts。两者的entries都会遍历所有键值对/元素逐个校验coercer则通过new Map(value)/new Set(value)克隆出新的实例测试 test/validation/map/valid.ts 展示了map(string(), number())的合法用例。两个类型工厂都支持省略子结构参数map()不校验键值对仅确认值是Mapset()不校验元素仅确认值是Set。官方文档提醒声明子结构时所有属性/元素都会被遍历以确保合法若不在意内部内容直接写map()/set()即可还能换取更好的性能。五、可空与可选nullable()/optional()nullable(string()) optional(string())nullable与optional都是对既有 struct 的增强器前者允许值额外为null后者允许值额外为undefined。它们不是新建类型而是通过包装validator与refiner实现的src/structs/types.ts 与 src/structs/types.tsexport function nullableT, S(struct: StructT, S): StructT | null, S { return new Struct({ ...struct, validator: (value, ctx) value null || struct.validator(value, ctx), refiner: (value, ctx) value null || struct.refiner(value, ctx), }) } export function optionalT, S(struct: StructT, S): StructT | undefined, S { return new Struct({ ...struct, validator: (value, ctx) value undefined || struct.validator(value, ctx), refiner: (value, ctx) value undefined || struct.refiner(value, ctx), }) }注意两者同时透传了 refinement值为null/undefined时直接放行否则继续执行原 struct 的 refine 逻辑。这就是optional(string())与min(1)等 refinement 叠加时仍能正确工作的原因。TypeScript 用户须知官方文档特别警告使用optional类型时必须在tsconfig.json中启用 TypeScript 的strictNullChecks选项Superstruct 才能正确处理 optional 类型。strictNullChecks默认关闭但开启strict后会自动启用。仓库内 TypeScript 使用指南见 docs/guides/06-using-typescript.md。六、组合类型union()与intersection()union()满足其一即可union([string(), number()])a string 42unionstruct 校验值至少匹配多个类型中的一个src/structs/types.ts。它的行为值得细看coercer依次对每个成员 struct 执行validate(value, { coerce: true, mask: ctx.mask })返回第一个通过校验的成员的结果——这意味着union可以参与默认值等强制转换场景。测试 test/validation/union/coercion.ts 展示了union([defaulted(string(), foo), number()])对undefined校验时输出foo的过程validator依次用每个成员 struct 运行校验只要有一个成功即整体通过全部失败时聚合返回Expected the value to satisfy a union of \string | number, but received: ... 以及各成员的失败明细schema为nulltype字段为union。intersection()全部满足才通过intersection([string(), Email])janeexample.comintersectionstruct 校验值必须同时匹配所有传入的 structsrc/structs/types.tsexport function intersectionA extends AnyStruct, B extends AnyStruct[](Structs: [A, ...B]): any { return new Struct({ type: intersection, schema: null, *entries(value, ctx) { for (const S of Structs) { yield* S.entries(value, ctx) } }, *validator(value, ctx) { for (const S of Structs) { yield* S.validator(value, ctx) } }, *refiner(value, ctx) { for (const S of Structs) { yield* S.refiner(value, ctx) } }, }) }从实现看intersection是透传式的它把entries、validator、refiner三个维度全部委托给所有成员 struct任何一个成员失败都会导致整体失败。上文示例中intersection([string(), Email])表示既是字符串又通过Email这个自定义类型校验——Email本身可用下文的自定义类型define创建也可来自 docs/reference/refinements.md 中的 refinement。七、特殊类型any、unknown与never这三个类型分别处理校验的三种极端。any() unknown() never()any()接受任何值src/structs/types.ts实现为define(any, () true)。官方文档提醒在 TypeScript 中使用any()会把类型放宽为any从而丧失类型安全建议改用unknown()unknown()同样接受任何值src/structs/types.ts实现也是恒真但不会把类型放宽为any——它保留unknown类型迫使你在使用前做类型收窄兼顾了不做校验与类型安全never()拒绝一切值src/structs/types.ts实现为define(never, () false)。它通常不直接使用而是被object()未知属性与tuple()多余元素内部当作必然失败的子 struct 引用如上文源码所示。八、自定义类型用define扩展你的校验体系当内置的 25 个类型无法满足业务需求时Superstruct 提供了define(name, validator)来定义应用专属的类型工厂见 docs/reference/types.md 与 src/structs/utilities.ts。官方文档给出了完整的实战示例import { define, object, string, number } from superstruct import isEmail from is-email import isUuid from is-uuid const Email define(Email, isEmail) const Uuid define(Uuid, (value) isUuid.v4(value)) const User object({ id: Uuid, name: string(), email: Email, age: number(), })自定义 validator 函数有两种合法返回形式返回true/false——最简单仅表达通过/不通过返回StructFailure对象数组——当需要更精确、更友好的错误消息时可以逐条产出失败详情StructFailure的字段结构与 docs/reference/errors.md 中描述的一致包含value、type、path、branch等。TypeScript 用户还可以为自定义类型传入泛型参数让类型收窄更精确const Email definestring(Email, isEmail)这样Email的推断类型就是string而非默认的unknownUser结构体在编译期就能获得正确的属性类型。历史备注在superstruct0.11之前该工厂函数名为struct旧名称被重命名为define且保留的struct()调用会打印迁移警告见 src/structs/utilities.ts。新代码请统一使用define。九、类型速查表下表汇总了全部内置类型工厂及其核心行为方便快速检索工厂合法值示例核心校验逻辑源码位置any()任意值恒真types.tsarray(Element?)[1, 2, 3]是数组元素匹配Element可省略types.tsbigint()3ntypeof value biginttypes.tsboolean()true/falsetypeof value booleantypes.tsdate()new Date()instanceof Date且!isNaN(getTime())types.tsenums([...])Jane属于给定字面量集合types.tsfunc()function () {}typeof value functiontypes.tsinstance(Class)new MyClass()value instanceof Classtypes.tsinteger()42number且Number.isIntegertypes.tsintersection([...])janeexample.com同时匹配全部成员types.tsliteral(v)42value vtypes.tsmap(K?, V?)new Map([...])是Map键值分别匹配可省略types.tsnever()无恒假types.tsnumber()3.14、Infinitynumber且非NaNtypes.tsnullable(struct)值或null原 struct 逻辑放行nulltypes.tsobject({...}){ id: 1 }对象且未知属性失败types.tsoptional(struct)值或undefined原 struct 逻辑放行undefinedtypes.tsrecord(K, V){ a: 1 }键匹配K、值匹配Vtypes.tsregexp()/\d/instanceof RegExp不测匹配types.tsset(Element?)new Set([...])是Set元素匹配可省略types.tsstring()validtypeof value stringtypes.tstuple([...])[a, 1, true]定长数组多余元素失败types.tstype({...})含声明属性的对象未知属性放行types.tsunion([...])a string/42至少匹配一个成员types.tsunknown()任意值恒真且不放宽类型types.ts十、组合示例把类型工厂用到真实场景最后用一个贴近业务的组合示例演示各类型的协作可直接在 examples 目录的示例基础上运行import { assert, array, boolean, enums, number, object, optional, string, type, union, } from superstruct const Role enums([admin, editor, viewer]) const Profile type({ nickname: string(), bio: optional(string()), age: union([number(), undefined]), active: boolean(), }) const User object({ id: number(), role: Role, profile: Profile, tags: array(string()), }) const payload { id: 1, role: admin, profile: { nickname: jane, bio: undefined, // optional 放行 undefined age: undefined, // union 放行 undefined active: true, extraProp: ok, // type 放行未知属性 }, tags: [typescript, superstruct], } assert(payload, User) // 通过若把payload.profile.extraProp换成顶层payload.extraPropassert会立刻抛出StructError因为顶层使用的是严格object()这正体现了object严格与type宽松的分工。延伸阅读类型工厂完整源码 与Struct基类理解validator/refiner/coercer/entries四段模型docs/reference/core.mdassert、is、validate、create、mask五个核心 API 的用法其中mask与object/type的行为有直接关联docs/reference/refinements.mdmin、max、pattern、size等 refinement 类型与本文的类型工厂叠加使用docs/reference/coercions.md 与 docs/reference/utilities.mddefaulted、trimmed等强制转换以及assign、omit、pick、partial、dynamic、lazy等工具类型docs/guides/06-using-typescript.mdTypeScript 下的Infer/Describe类型推导与strictNullChecks配置说明。赞分享开发工具【免费下载链接】superstructA simple and composable way to validate data in JavaScript (and TypeScript).项目地址https://gitcode.com/gh_mirrors/su/superstruct点击查看免费下载相关推荐Pydantic类型系统内置类型与自定义类型Pydantic类型系统内置类型与自定义类型 Pydantic 提供了强大的类型系统全面支持标准Python内置类型、特有类型、约束类型以及灵活的自定义类型后端序列化SQLAlchemy 数据类型体系全解TypeEngine 层次、内置类型与自定义类型实战SQLAlchemy 数据类型体系全解TypeEngine 层次、内置类型与自定义类型实战 SQLAlchemy 通过一套以 TypeEngine 为根基的数据库后端ORMMikroORM 自定义类型Custom Types实战指南扩展 Type 抽象类与内置类型全解析MikroORM 自定义类型Custom Types实战指南扩展 Type 抽象类与内置类型全解析 MikroORM 允许开发者通过继承 Type 抽象类后端上一篇免费在线3D查看器终极指南浏览器中轻松预览和测量任何3D设计文件下一篇WinPython终极指南打造Windows上最便携的Python科学计算环境创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表