news 2026/9/29 6:54:17

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Superstruct 类型系统全解析:25 个内置类型工厂与自定义类型的实战指南
  • 开发工具

【免费下载链接】superstruct

A 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.md);
  • coercer:在校验前把输入值转换为目标形态(如数组浅拷贝、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 define<T>(name: string, validator: Validator): Struct<T, null> { return new Struct({ type: name, schema: null, validator }) }

也就是说,像string()、number()这些"一行实现",本质上是"给一段校验函数起个名字"。理解这一点后,下面每个类型的行为都会非常直观。

二、基础标量类型

string()

string()
'valid'

stringstruct 校验值是否为字符串。源码实现(src/structs/types.ts):

export function string(): Struct<string, 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 Infinity

numberstruct 校验值是否为数字(src/structs/types.ts):

export function number(): Struct<number, null> { return define('number', (value) => { return ( (typeof value === 'number' && !isNaN(value)) || `Expected a number, but received: ${print(value)}` ) }) }

从源码可以得出两个精确的边界行为:

  • NaN会被拒绝——因为isNaN(NaN)为true;
  • Infinity会被接受——isNaN(Infinity)为false,且官方文档的合法值示例中明确列出了Infinity。

bigint()

bigint()
0n 3n 4000030n BigInt(10n ^ 1000n)

bigintstruct 校验值是否为 bigint(src/structs/types.ts),实现即typeof value === 'bigint'。对应测试见 test/validation/bigint/valid.ts 与 test/validation/bigint/invalid.ts。

boolean()

boolean()
true false

booleanstruct 只接受布尔值true和false(src/structs/types.ts),实现为typeof value === 'boolean'。注意0、1、'true'等"类布尔"值并不会被隐式转换,一律校验失败。

integer()

integer()
-7 0 42

integerstruct 校验值是否为整数(src/structs/types.ts):

export function integer(): Struct<number, 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)
42

literalstruct 使用===运算符强制值必须精确等于给定常量(src/structs/types.ts):

export function literal<T>(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' 'John'

enumsstruct 校验值是否为给定字面量集合中的一员(src/structs/types.ts):

export function enums<U 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)}` ) }, }) }

两个实现细节值得注意:

  1. schema 是可访问的:源码注释说明创建后可读取struct.schema获取候选值集合(形如{ Jane: 'Jane', John: 'John', ... });
  2. 支持数字与字符串混合集合:重载签名限定为string | number。

对应测试见 test/validation/enums/valid.ts。

三、时间与正则类型

date()

date()
new Date()

datestruct 校验值是否为 JavaScriptDate实例(src/structs/types.ts):

export function date(): Struct<Date, 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#pattern):

import { 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.ts):

export function array<T extends Struct<any>>(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.ts):

export function tuple<A 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.ts):

export function object<S 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)}` }, ... }) }

源码揭示了两个值得展开的行为:

  1. 多余属性必然失败:所有未在 schema 中声明的属性都会被收集到unknowns集合,并交给never()校验。测试 test/validation/object/invalid-property-unknown.ts 显示,{ name: 'john', age: 42, unknown: true }中unknown属性产生type: 'never'、path: ['unknown']的失败。官方文档指出:若不想对多余属性报错,应改用type;若想在宽容输入的同时丢弃多余属性,则用mask(mask触发时,object的coercer会直接从拷贝对象中删除未声明属性,见 src/structs/types.ts);
  2. 数组不是对象: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.ts):

export function type<S extends ObjectSchema>(schema: S): Struct<ObjectType<S>, 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.ts):

export function record<K extends string, V>(Key: Struct<K>, Value: Struct<V>): 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 的Record<K, 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.ts);setstruct 校验值为Set实例,且元素匹配指定类型(src/structs/types.ts)。两者的entries都会遍历所有键值对/元素逐个校验,coercer则通过new Map(value)/new Set(value)克隆出新的实例(测试 test/validation/map/valid.ts 展示了map(string(), number())的合法用例)。

两个类型工厂都支持省略子结构参数:

  • map():不校验键值对,仅确认值是Map;
  • set():不校验元素,仅确认值是Set。

官方文档提醒:声明子结构时所有属性/元素都会被遍历以确保合法;若不在意内部内容,直接写map()/set()即可,还能换取更好的性能。

五、可空与可选:nullable()/optional()

nullable(string()) optional(string())

nullable与optional都是对既有 struct 的增强器:前者允许值额外为null,后者允许值额外为undefined。它们不是新建类型,而是通过包装validator与refiner实现的(src/structs/types.ts 与 src/structs/types.ts):

export function nullable<T, S>(struct: Struct<T, S>): Struct<T | 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 optional<T, S>(struct: Struct<T, S>): Struct<T | 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' 42

unionstruct 校验值至少匹配多个类型中的一个(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为null,type字段为'union'。

intersection():全部满足才通过

intersection([string(), Email])
'jane@example.com'

intersectionstruct 校验值必须同时匹配所有传入的 struct(src/structs/types.ts):

export function intersection<A 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 函数有两种合法返回形式:

  1. 返回true/false——最简单,仅表达"通过/不通过";
  2. 返回StructFailure对象数组——当需要更精确、更友好的错误消息时,可以逐条产出失败详情(StructFailure的字段结构与 docs/reference/errors.md 中描述的一致,包含value、type、path、branch等)。

TypeScript 用户还可以为自定义类型传入泛型参数,让类型收窄更精确:

const Email = define<string>('Email', isEmail)

这样Email的推断类型就是string而非默认的unknown,User结构体在编译期就能获得正确的属性类型。

历史备注:在superstruct@0.11之前该工厂函数名为struct;旧名称被重命名为define,且保留的struct()调用会打印迁移警告(见 src/structs/utilities.ts)。新代码请统一使用define。

九、类型速查表

下表汇总了全部内置类型工厂及其核心行为,方便快速检索:

工厂合法值示例核心校验逻辑(源码位置)
any()任意值恒真(types.ts)
array(Element?)[1, 2, 3]是数组;元素匹配Element(可省略)(types.ts)
bigint()3ntypeof value === 'bigint'(types.ts)
boolean()true/falsetypeof value === 'boolean'(types.ts)
date()new Date()instanceof Date且!isNaN(getTime())(types.ts)
enums([...])'Jane'属于给定字面量集合(types.ts)
func()function () {}typeof value === 'function'(types.ts)
instance(Class)new MyClass()value instanceof Class(types.ts)
integer()42number且Number.isInteger(types.ts)
intersection([...])'jane@example.com'同时匹配全部成员(types.ts)
literal(v)42value === v(types.ts)
map(K?, V?)new Map([...])是Map;键值分别匹配(可省略)(types.ts)
never()无恒假(types.ts)
number()3.14、Infinitynumber且非NaN(types.ts)
nullable(struct)值或null原 struct 逻辑放行null(types.ts)
object({...}){ id: 1 }对象且未知属性失败(types.ts)
optional(struct)值或undefined原 struct 逻辑放行undefined(types.ts)
record(K, V){ a: 1 }键匹配K、值匹配V(types.ts)
regexp()/\d+/instanceof RegExp,不测匹配(types.ts)
set(Element?)new Set([...])是Set;元素匹配(可省略)(types.ts)
string()'valid'typeof value === 'string'(types.ts)
tuple([...])['a', 1, true]定长数组,多余元素失败(types.ts)
type({...})含声明属性的对象未知属性放行(types.ts)
union([...])'a string'/42至少匹配一个成员(types.ts)
unknown()任意值恒真且不放宽类型(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.extraProp,assert会立刻抛出StructError,因为顶层使用的是严格object();这正体现了object(严格)与type(宽松)的分工。

延伸阅读

  • 类型工厂完整源码 与Struct基类:理解validator/refiner/coercer/entries四段模型;
  • docs/reference/core.md:assert、is、validate、create、mask五个核心 API 的用法,其中mask与object/type的行为有直接关联;
  • docs/reference/refinements.md:min、max、pattern、size等 refinement 类型,与本文的类型工厂叠加使用;
  • docs/reference/coercions.md 与 docs/reference/utilities.md:defaulted、trimmed等强制转换,以及assign、omit、pick、partial、dynamic、lazy等工具类型;
  • docs/guides/06-using-typescript.md:TypeScript 下的Infer/Describe类型推导与strictNullChecks配置说明。
  • 开发工具

【免费下载链接】superstruct

A simple and composable way to validate data in JavaScript (and TypeScript).

项目地址:https://gitcode.com/gh_mirrors/su/superstruct
点击查看免费下载
上一篇:免费在线3D查看器终极指南:浏览器中轻松预览和测量任何3D设计文件
下一篇:WinPython终极指南:打造Windows上最便携的Python科学计算环境

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 6:51:24

Go 性能调优实战:pprof + trace + benchmem 三件套

Go 性能调优实战&#xff1a;pprof trace benchmem 三件套写完 Go 服务后&#xff0c;下一步是把性能调起来。本文以案例驱动讲 pprof、trace、benchmark 的实战套路。一、pprof 三件套 import _ "net/http/pprof" go http.ListenAndServe(":6060", nil)…

作者头像 李华