ARTICLE DETAIL

资讯详情

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

io-ts Decoder 模块完全指南:从运行时解码到类型安全的数据校验

io-ts Decoder 模块完全指南:从运行时解码到类型安全的数据校验 后端【免费下载链接】io-tsRuntime type system for IO decoding/encoding项目地址https://gitcode.com/gh_mirrors/io/io-ts点击查看免费下载本指南围绕 io-ts 2.2.x 的Decoder模块展开它是 io-ts 中用于**运行时解码IO decoding**的核心抽象把unknown输入例如 HTTP 请求体、本地存储读取结果安全地转换为具有静态类型的领域模型。读完本文你将掌握Decoder的模型与解码流程、全部内置原始解码器与组合子的用法、如何从解码器反向提取 TypeScript 静态类型以及如何使用内置错误报告器输出结构化的错误树。Decoder模块在 io-ts 中属于experimental实验性特性自 v2.2.7 引入当前仓库版本为 2.2.21其接口可能随版本演进变化使用前建议关注仓库 CHANGELOG。Decoder 模型一次解码一份 EitherDecoder的核心模型非常简洁只有一个decode方法import * as E from fp-ts/Either interface DecoderI, A { readonly decode: (i: I) E.EitherDecodeError, A }其中I是输入类型A是解码成功后的输出类型。在 src/Decoder.ts 中DecoderI, A被定义为K.KleisliE.URI, I, DecodeError, A的别名即它是基于 fp-tsEither的 Kleisli 箭头输入I要么返回LeftDecodeError解码失败携带结构化的错误要么返回RightA解码成功。一个最朴素的自定义解码器——代表string——可以这样手工定义import * as D from io-ts/Decoder export const string: D.Decoderunknown, string { decode: (u) (typeof u string ? D.success(u) : D.failure(u, string)) }这里D.success(a)等价于E.right(a)而D.failure(actual, message)会构造一个E.left(error(actual, message))其实现见 src/Decoder.ts。随后可以这样使用import { isRight } from fp-ts/Either console.log(isRight(string.decode(a))) // true console.log(isRight(string.decode(null))) // false更一般地decode的返回结果可以用 fp-ts 的fold配合pipe类似于管道运算符来处理成功与失败两个分支import { pipe } from fp-ts/function import { fold } from fp-ts/Either console.log( pipe( string.decode(null), fold( // failure handler (errors) error: ${JSON.stringify(errors)}, // success handler (a) success: ${JSON.stringify(a)} ) ) ) // error: {_tag:Of,value:{_tag:Leaf,actual:null,error:string}}注意失败分支返回的不是普通字符串而是一个DecodeError数据结构外层_tag: Of来自自由半群FreeSemigroup见 src/FreeSemigroup.ts内层_tag: Leaf记录实际输入actual与错误消息error其模型定义在 src/DecodeError.ts。除Leaf外DecodeError还包括Key对象属性、Index数组索引、Member联合成员序号、Lazy递归类型、Wrap自定义消息包装六种节点它们共同构成一棵可递归组合的错误树。内置原始解码器Primitives模块开箱即用提供五个原始解码器全部输入为unknown实现位于 src/Decoder.ts解码器类型说明stringDecoderunknown, string输入必须是stringnumberDecoderunknown, number输入必须是number且排除NaN测试用例见 test/Decoder.tsbooleanDecoderunknown, boolean输入必须是booleanUnknownArrayDecoderunknown, Arrayunknown输入必须是数组元素不做进一步校验UnknownRecordDecoderunknown, Recordstring, unknown输入必须是对象非 null字段不做进一步校验从源码看前三个原始解码器均由fromGuard(G.string, string)之类的构造器生成src/Decoder.ts即把 src/Guard.ts 中的类型守卫包装成解码器而UnknownArray与UnknownRecord则直接由G.UnknownArray、G.UnknownRecord守卫转换而来。这意味着它们的判定逻辑与 Guard 模块完全一致——例如UnknownArray会拒绝null错误消息为ArrayunknownUnknownRecord会拒绝null错误消息为Recordstring, unknown这些行为均有测试覆盖test/Decoder.ts。组合子Combinators从原始类型到领域模型原始解码器只能处理单一值。真实应用中我们通过**组合子combinator**把它们组合成复合类型用来描述领域模型、请求体request payload等。下面的每一个组合子都直接对应 src/Decoder.ts 中的一个同名导出。literal构造器描述一个或多个字面量literal用来描述固定的字面量值一个或多个export const MyLiteral: D.Decoderunknown, a D.literal(a) export const MyLiterals: D.Decoderunknown, a | b D.literal(a, b)其实现位于 src/Decoder.ts底层是K.literal并且错误消息会列出所有允许值并用|连接。例如D.literal(a, null)解码b时产生的Leaf错误消息为a | null对应测试 test/Decoder.ts。注意literal也接受非字符串字面量如D.literal(a, null, b, 1, true)在测试中被验证可正确解码a与null。nullable组合子可空值nullable描述要么是null要么是给定类型export const NullableString: D.Decoderunknown, null | string D.nullable(D.string)其实现src/Decoder.ts通过K.nullable生成当输入既不是null也不满足内层解码器时会同时产生两个Member错误序号 0 报null分支失败、序号 1 报内层解码器失败然后用FS.concat合并。测试test/Decoder.ts验证了D.nullable(...)对null、合法值、undefined三种输入的行为差异undefined会被判定为既不是 null 也不是 string。struct组合子必填字段对象struct描述一个所有字段必填的对象export const Person D.struct({ name: D.string, age: D.number }) console.log(isRight(Person.decode({ name: name, age: 42 }))) // true console.log(isRight(Person.decode({ name: name }))) // false关键行为解码时会剥离strip额外的字段console.log(Person.decode({ name: name, age: 42, rememberMe: true })) // { _tag: Right, right: { name: name, age: 42 } }从源码看struct实际上是pipe(UnknownRecord, compose(fromStruct(properties)))src/Decoder.ts先确认输入是对象再用fromStruct逐字段解码。fromStructsrc/Decoder.ts为每个字段错误包装DE.key(k, DE.required, e)即带required标记的Key节点并且多个字段的错误会全部收集ap在左右两侧都是Left时用SE.concat合并错误见 src/Decoder.ts对应测试 test/Decoder.ts。另外测试还表明struct支持 getter 对象test/Decoder.ts但对undefined输入会直接报Recordstring, unknown错误test/Decoder.ts。partial组合子可选字段对象partial描述一个所有字段可选的对象export const Person D.partial({ name: D.string, age: D.number }) console.log(isRight(Person.decode({ name: name, age: 42 }))) // true console.log(isRight(Person.decode({ name: name }))) // true同样会剥离额外字段console.log(Person.decode({ name: name, rememberMe: true })) // { _tag: Right, right: { name: name } }实现为pipe(UnknownRecord, compose(fromPartial(properties)))src/Decoder.ts其中fromPartialsrc/Decoder.ts对字段错误包装DE.key(k, DE.optional, e)——与struct的唯一区别是Key节点的kind为optional。测试确认partial不会为缺失字段补默认值decode({})得到{}但不会剥离值为undefined的字段test/Decoder.ts。record组合子键值对字典record描述Recordstring, ?即所有键的值都必须是给定类型export const MyRecord: D.Decoderunknown, Recordstring, number D.record(D.number) console.log(isRight(MyRecord.decode({ a: 1, b: 2 }))) // true实现为pipe(UnknownRecord, compose(fromRecord(codomain)))src/Decoder.tsfromRecordsrc/Decoder.ts对每个键的值错误包装DE.key(k, DE.optional, e)。当解码{ a: 1, b: b }值为number的 record时两个键的错误会一并收集测试 test/Decoder.ts 及后续用例验证了错误收集行为。array组合子数组array描述Array?export const MyArray: D.Decoderunknown, Arraynumber D.array(D.number) console.log(isRight(MyArray.decode([1, 2, 3]))) // true实现为pipe(UnknownArray, compose(fromArray(item)))src/Decoder.tsfromArraysrc/Decoder.ts对每个元素错误包装DE.index(i, DE.optional, e)。测试验证了元素错误按索引收集解码[1, 2]期望字符串数组会同时产生 index 0 与 index 1 两个错误test/Decoder.ts。tuple组合子n 元组tuple描述固定长度的 n 元组export const MyTuple: D.Decoderunknown, [string, number] D.tuple(D.string, D.number) console.log(isRight(MyTuple.decode([a, 1]))) // true解码时会剥离多余的分量console.log(MyTuple.decode([a, 1, true])) // { _tag: Right, right: [ a, 1 ] }实现为pipe(UnknownArray, compose(fromTuple(...components)))src/Decoder.tsfromTuplesrc/Decoder.ts对每个分量错误包装DE.index(i, DE.required, e)——注意 tuple 的元素错误标记为required与 array 的optional不同。intersect组合子交叉类型intersect用于把多个解码器的约束合并典型场景是混合必填与可选字段import { pipe } from fp-ts/function export const Person pipe( D.struct({ name: D.string }), D.intersect( D.partial({ age: D.number }) ) ) console.log(isRight(Person.decode({ name: name }))) // true console.log(isRight(Person.decode({}))) // false其类型签名是IB, B(right: DecoderIB, B) IA, A(left: DecoderIA, A) DecoderIA IB, A Bsrc/Decoder.ts输入与输出类型都取交叉。底层基于K.intersect它要求左右两个解码器都通过才返回成功。sum组合子带标签联合sum typesum描述带判别标签的联合类型是最常用的可辨识联合建模工具export const MySum: D.Decoder unknown, | { type: A a: string } | { type: B b: number } // v--- tag name D.sum(type)({ // ----- all union members in the dictionary must own a field named like the chosen tag (type in this case) // | // v v----- this value must be equal to its corresponding dictionary key (A in this case) A: D.struct({ type: D.literal(A), a: D.string }), // v----- this value must be equal to its corresponding dictionary key (B in this case) B: D.struct({ type: D.literal(B), b: D.number }) })使用约定原文档强调类型签名也强制保证字典中每个成员必须拥有与所选 tag 同名的字段且该字段的字面量值必须等于其在字典中的键。解码时sum先确认输入是对象pipe(UnknownRecord, compose(fromSum(tag)(members)))src/Decoder.ts再根据 tag 字段的值分派到对应成员如果 tag 值不在已知键集合中fromSumsrc/Decoder.ts会生成DE.key(tag, DE.required, error(value, keys.join( | )))错误——即错误消息会把所有合法 tag 值用|列出集合为空时消息为never。非字符串 tag 值当 tag 值为非字符串如数字时字典的键需要用方括号包裹export const MySum: D.Decoder unknown, | { type: 1 // non-string tag value a: string } | { type: 2 // non-string tag value b: number } D.sum(type)({ [1]: D.struct({ type: D.literal(1), a: D.string }), [2]: D.struct({ type: D.literal(2), b: D.number }) })union组合子无标签联合union描述无标签的联合类型——依次尝试每个成员任一成功即成功const MyUnion D.union(D.string, D.number) console.log(isRight(MyUnion.decode(a))) // true console.log(isRight(MyUnion.decode(1))) // true console.log(isRight(MyUnion.decode(null))) // false实现为K.union(M)((i, e) FS.of(DE.member(i, e)))src/Decoder.ts每个失败的成员对应一个带序号i的Member错误。union至少需要一个成员类型签名要求readonly [Decoder, ...Decoder[]]。lazy组合子递归与相互递归lazy允许定义递归和相互递归的解码器解决类型引用自身的循环依赖问题递归interface Category { title: string subcategory: null | Category } const Category: D.Decoderunknown, Category D.lazy(Category, () D.struct({ title: D.string, subcategory: D.nullable(Category) }) )lazy的第一个参数id是调试用的名字第二个参数是惰性的工厂函数只在解码时被调用。其实现src/Decoder.ts会为惰性求值过程中的错误包装DE.lazy(id, e)节点使错误树可读。相互递归interface Foo { foo: string bar: null | Bar } interface Bar { bar: number foo: null | Foo } const Foo: D.Decoderunknown, Foo D.lazy(Foo, () D.struct({ foo: D.string, bar: D.nullable(Bar) }) ) const Bar: D.Decoderunknown, Bar D.lazy(Bar, () D.struct({ bar: D.number, foo: D.nullable(Foo) }) )注意这里Foo/Bar既是接口名又是常量名TypeScript 允许这种声明合并 同名常量的惯用法。refine组合子精炼refinementrefine在已有解码器之上施加一个类型守卫典型用途是定义品牌类型branded typeimport { pipe } from fp-ts/function export interface PositiveBrand { readonly Positive: unique symbol } export type Positive number PositiveBrand export const Positive: D.Decoderunknown, Positive pipe( D.number, D.refine((n): n is Positive n 0, Positive) ) console.log(isRight(Positive.decode(1))) // true console.log(isRight(Positive.decode(-1))) // false实现为K.refine(M)(refinement, (a) error(a, id))src/Decoder.ts输入先经过被精炼的解码器这里是D.number再应用refinement守卫返回false时产生Leaf错误消息为传入的id如Positive。refine不改变输出值的形状只收窄其类型。parse组合子改变输出类型的解析parse比refine更强大——它可以在解码过程中完全改变输出类型不只是收窄import { pipe } from fp-ts/function import { isRight } from fp-ts/Either export const NumberFromString: D.Decoderunknown, number pipe( D.string, D.parse((s) { const n parseFloat(s) return isNaN(n) ? D.failure(s, NumberFromString) : D.success(n) }) ) console.log(isRight(NumberFromString.decode(1))) // true console.log(isRight(NumberFromString.decode(a))) // false其签名src/Decoder.ts要求传入一个(a: A) E.EitherDecodeError, B的解析函数——你可以返回D.success(n)或D.failure(actual, message)也可以复用error/success自由构造更复杂的失败结构。这使parse成为实现字符串 → 数字字符串 → 日期等格式转换型校验的标准入口。从解码器提取静态类型TypeOf与InputOf解码器不仅是运行时校验器还能反过来推导出 TypeScript 静态类型——这是 io-ts 类型安全的核心价值。使用TypeOf操作符export const Person D.struct({ name: D.string, age: D.number }) export type Person D.TypeOftypeof Person /* type Person { name: string; age: number; } */ type PersonInputType D.InputOftypeof Person /* type PersonInputType unknown */TypeOfD解码成功后的输出类型即DecoderI, A中的A其定义见 src/Decoder.tsInputOfD解码器的输入类型即DecoderI, A中的I定义见 src/Decoder.ts。上例中因为struct的输入是unknown所以InputOf推导出unknown而如果使用fromStruct这类输入类型保留的构造器InputOf会精确推导出每个字段的输入类型。二者的底层实现是 Kleisli 层的K.TypeOf/K.InputOf与 io-ts 类型类体系打通。除了类型别名你也可以用interface声明export interface Person extends D.TypeOftypeof Person {}这在需要为解码结果附加方法或与现有interface合并的场景中很常见注意与sum一节中接口 常量同名用法的区别这里是接口继承类型。内置错误报告器drawdecode失败时返回的DecodeError是一棵结构化错误树直接用JSON.stringify并不友好。模块提供draw函数把它渲染成可读的多行文本import { isLeft } from fp-ts/Either export const Person D.struct({ name: D.string, age: D.number }) const result Person.decode({}) if (isLeft(result)) { console.log(D.draw(result.left)) } /* required property name └─ cannot decode undefined, should be string required property age └─ cannot decode undefined, should be number */draw的实现位于 src/Decoder.ts先用DE.fold把六种错误节点Leaf、Key、Index、Member、Lazy、Wrap逐一映射为树节点文本其中Leaf渲染为cannot decode actual, should be errorKey渲染为kind property keykind为required或optionalIndex渲染为kind index nMember渲染为member nLazy渲染为lazy type id随后用drawTree/drawForest按├─/└─/│绘制树形缩进。对于嵌套结构如 struct 里的数组、联合里的成员draw会递归输出完整路径。draw的视觉效果在withMessage的测试用例中有完整样例test/Decoder.ts该用例同时展示了它与withMessage组合后错误树顶部会多出一层自定义标题。进阶工具withMessage、compose、id与alt除文档主线内容外Decoder模块还提供了几个高频使用的组合子这里结合源码补充withMessage自定义错误消息在任意解码器外层包裹一个自定义消息消息函数可拿到原始输入input与错误eimport { pipe } from fp-ts/function const Person pipe( D.struct({ name: D.string, age: D.number }), D.withMessage(() Person) )实现src/Decoder.ts通过mapLeftWithInput把错误包装为DE.wrap(message(input, e), e)——即Wrap节点draw时会把它渲染成错误树的一层标题测试输出见 test/Decoder.ts。mapLeftWithInput直接改写错误withMessage的底层允许用任意函数(input, e) DecodeError整体替换错误例如pipe(_.number, _.mapLeftWithInput((u) FS.of(DE.leaf(u, not a number))))测试见 test/Decoder.ts。compose与id解码器作为范畴Decoder具有Category实例src/Decoder.tscompose(to)把解码器像函数一样串联pipe(_.number, _.compose(fromRefinement(...)))可逐级细化校验测试见 test/Decoder.tsidA()是恒等解码器DecoderA, A直接放行。alt按需回退Alt实例src/Decoder.ts提供alt(that)当前一个解码器失败时尝试备选解码器that。它与union的差别在于alt是顺序尝试 惰性构造that是函数适合表达先试 A失败再试 B的优先级语义。版本与兼容性说明Decoder模块自v2.2.7起作为实验性功能提供fromStruct、struct、readonly等为后续版本v2.2.8 / v2.2.15追加当前仓库版本为2.2.21见 package.json。模块 API 文档与签名速查见 docs/modules/Decoder.ts.md。旧命名type与fromType已废弃deprecated应分别改用struct与fromStructsrc/Decoder.ts、src/Decoder.ts。模块依赖fp-ts ^2.5.0peer dependency示例中的Either、pipe、fold、isRight均来自 fp-ts建议搭配使用。运行测试可执行npm run vitestvitest 测试或npm run mocha其中Decoder全部行为用例位于 test/Decoder.ts。小结Decoder是 io-ts 解码链路上的第一环decode把unknown输入映射为EitherDecodeError, A失败时产生可递归渲染的错误树配合struct/partial/record/array/tuple/sum/union等组合子可以完整建模常见领域结构配合lazy/refine/parse可以表达递归、品牌类型与格式转换配合TypeOf/InputOf又能把运行时校验与编译期类型无缝对齐。理解了本模块再去看Codec解码 编码一体、Encoder或TaskDecoder异步解码等模块时会发现它们共享同一套组合子思维与错误模型。赞分享后端【免费下载链接】io-tsRuntime type system for IO decoding/encoding项目地址https://gitcode.com/gh_mirrors/io/io-ts点击查看免费下载相关推荐5分钟快速上手Messaging APIs构建你的第一个跨平台机器人5分钟快速上手Messaging APIs构建你的第一个跨平台机器人 想要快速构建一个能在多个消息平台上运行的机器人吗Messaging APIs 正是你需fp-ts Refinement 模块实战指南用类型谓词构建类型安全的运行时校验fp ts Refinement 模块实战指南用类型谓词构建类型安全的运行时校验 Refinement 是 fp ts 在 v2.11.0 引入的独立模块围开发工具使用 Genkit Go 版 DashScope 插件接入阿里云 Qwen 模型配置、模型目录与兼容模式全解析使用 Genkit Go 版 DashScope 插件接入阿里云 Qwen 模型配置、模型目录与兼容模式全解析 本文面向使用 GenkitGo构建 Age后端上一篇Seraphine基于LCU API的英雄联盟智能数据分析解决方案下一篇Horizon 的 AI 创作者雷达ai-creator画像从匹配规则到评分与内容角度的完整解读创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表