十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Effect Schema `toTaggedUnion` 自定义判别键类型收窄修复:`isAnyOf` 与运行时行为对齐

Effect Schema `toTaggedUnion` 自定义判别键类型收窄修复:`isAnyOf` 与运行时行为对齐 Effect SchematoTaggedUnion自定义判别键类型收窄修复isAnyOf与运行时行为对齐【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect导读本文基于 Effect 仓库中编号为 #2386 的修复见 .changeset/pre/fix-to-tagged-union-isanyof-custom-tags.md深入讲解Schema.toTaggedUnion(...)在自定义判别键discriminant key下isAnyOf类型收窄narrowing的历史缺陷与修复方案。你将理解toTaggedUnion的运行时行为为何始终正确、类型层面为何长期按_tag硬编码提取成员、修复后的isAnyOf类型签名如何与运行时保持一致以及如何借助TaggedUnion/toTaggedUnion编写既安全又简洁的判别联合类型代码。说明本文讨论的 API 位于 packages/effect/src/Schema.ts其中toTaggedUnion与TaggedUnion均标记为since 4.0.0对应的类型收窄修复补丁为effect: patch级别的非破坏性变更。背景Effect Schema 的判别联合与toTaggedUnion在 Effect Schema 中判别联合discriminated/tagged union是描述领域模型的常用手段。一个典型的做法是先定义若干带字面量标签字段的结构再通过Schema.Union组合它们最后调用Schema.toTaggedUnion(_tag)为联合补充一组类型安全的使用工具import { Schema } from effect const A Schema.TaggedStruct(A, { value: Schema.Number }) const B Schema.TaggedStruct(B, { name: Schema.String }) const MyUnion Schema.Union([A, B]).pipe(Schema.toTaggedUnion(_tag)) // 模式匹配 const result MyUnion.match({ _tag: A, value: 1 }, { A: (a) number: ${a.value}, B: (b) name: ${b.name} }) result // number: 1上述示例与 Schema.ts 中toTaggedUnion的文档示例一致。从源码看toTaggedUnion的完整返回类型是export type toTaggedUnion Tag extends PropertyKey, Members extends ReadonlyArrayConstraint { readonly Type: { readonly [K in Tag]: PropertyKey } } UnionMembers TaggedUnionUtilsTag, Members即在原始Union之上交叉intersect一组工具方法这组工具由TaggedUnionUtils提供包括discriminants按成员扁平化顺序排列的判别值元组cases以判别值为键、成员 Schema 为值的映射isAnyOf给定一组判别值返回一个类型谓词用于判断值是否属于其中的某个变体guards每个变体对应的完整运行时守卫match/matchOrElse两种形式的模式匹配。对应的运行时实现位于 Schema.tswalk递归遍历含嵌套 Union并收集collectSentinels中的字面量判别值match/matchOrElse通过value[tag]取值并在cases中查找处理函数而isAnyOf的实现极为简洁const isAnyOf (keys: ReadonlyArrayPropertyKey) (value: Members[number][Type]) keys.includes(value[tag])可以看到运行时从一开始就使用toTaggedUnion(tag)传入的tag来读取判别值无论 tag 是_tag、kind还是event行为都保持一致。缺陷定位类型谓词为何只认_tag虽然运行时正确使用了自定义判别键但在本次修复之前isAnyOf的类型签名却始终按_tag提取联合成员。修复前的TaggedUnionUtils中isAnyOf的签名形如readonly isAnyOf: const Keys( keys: ReadonlyArrayKeys ) (value: Members[number][Type]) value is ExtractMembers[number][Type], { _tag: Keys }问题一目了然Extract..., { _tag: Keys }中硬编码了_tag。当开发者像下面这样使用自定义判别键kind时就产生了运行时与类型层面的不一致const schema Schema.Union([ Schema.Struct({ kind: Schema.tag(a), a: Schema.Number }), Schema.Struct({ kind: Schema.tag(b), b: Schema.String }), Schema.Struct({ kind: Schema.tag(c), c: Schema.Boolean }) ]).pipe(Schema.toTaggedUnion(kind)) const isAOrB schema.isAnyOf([a, b]) // 运行时正确但类型收窄失效运行时keys.includes(value[kind])判断kind是否为a/b完全正确类型层面Extract联合, { _tag: Keys }试图用_tag字段提取成员而这些结构体根本没有_tag字段Extract结果退化通常收窄为never或不产生任何收窄isAnyOf作为类型谓词的功能随之失效。这正是 changeset 中 Previously, the type predicate always extracted union members by_tag, even whentoTaggedUnionwas created with a different discriminant key 所描述的现象。修复方案让类型收窄复用自定义判别键修复方式是在TaggedUnionUtils的类型签名中用泛型参数Tag替换硬编码的_tag使类型层面的提取逻辑与运行时读取逻辑严格对齐。修复后的签名见 Schema.ts为readonly isAnyOf: const Keys( keys: ReadonlyArrayKeys ) (value: Members[number][Type]) value is Extract Members[number][Type], { readonly [K in Tag]: Keys } 这里{ readonly [K in Tag]: Keys }表示在Tag指定的键上取值属于Keys的成员。由于Tag正是创建toTaggedUnion时传入的判别键类型收窄与运行时的value[tag]读取天然一致。同时TaggedUnion_tag专用快捷方式中isAnyOf的签名保持{ _tag: Keys }不变见 Schema.ts因为它本身就是以_tag为唯一判别键的构造器——两者分工明确互不干扰。类型级验证typetest 中的回归用例仓库的类型测试 Union.tst.ts 为该修复提供了直接的回归证据it(isAnyOf should narrow custom tags, () { const schema Schema.Union([ Schema.Struct({ kind: Schema.tag(a), a: Schema.Number }), Schema.Struct({ kind: Schema.tag(b), b: Schema.String }), Schema.Struct({ kind: Schema.tag(c), c: Schema.Boolean }) ]).pipe(Schema.toTaggedUnion(kind)) const value holeSchema.Schema.Typetypeof schema() if (schema.isAnyOf([a, b])(value)) { expect(value).type.toBe | { readonly kind: a; readonly a: number } | { readonly kind: b; readonly b: string } () } })在if分支内value被精确收窄为kind: a | b的两个成员c分支被排除。修复之前该断言无法通过——因为Extract基于不存在的_tag字段无法正确提取修复之后类型收窄立即生效。同一文件还验证了toTaggedUnion接受_tag而拒绝不匹配的判别键expect(original.pipe).type.not.toBeCallableWith(Schema.toTaggedUnion(a))见 Union.tst.ts说明判别键的约束在构造阶段就已由类型系统强制。运行时验证测试用例与边界行为运行时行为在 Schema.test.ts 中有完整覆盖这些用例在修复前后行为一致可用于验证运行时本就正确这一结论默认_tag键isAnyOf([A, 1])对{ _tag: A }与{ _tag: 1 }返回true对{ _tag: D }与{ _tag: b }unique symbol返回falseSchema.test.ts自定义判别键多标签toTaggedUnion(type)时cases按type字段的TypeA/TypeB索引Schema.test.ts重复判别值报错无论是字符串重复还是1与1这种形近值都会抛出Duplicate discriminant错误Schema.test.ts空联合Schema.Union([]).pipe(Schema.toTaggedUnion(event))的discriminants为[]Schema.test.ts__proto__作为判别值cases/guards使用Object.hasOwn安全地处理该键Schema.test.ts类成员Schema.Class派生类的联合同样可以被toTaggedUnion增强Schema.test.ts。这些用例印证了walk实现中对嵌套 Union 的递归扁平化schema.members.forEach(walk)、数字判别值到字符串键的归一化typeof literal number ? String(literal) : literal以及cases/guards通过InternalRecord.assignProperty的显式赋值。实战建议优先使用TaggedUnion_tag快捷方式当不需要自定义判别键时Schema.TaggedUnion({ A: {...}, B: {...} })内部就是对toTaggedUnion(_tag)的封装见 Schema.ts代码更简洁。需要自定义判别键时如既有数据使用kind、event、type等字段使用toTaggedUnion(kind)并确认修复版本已包含本次补丁isAnyOf的类型收窄才能与运行时一致。把isAnyOf当作类型谓词使用在if (schema.isAnyOf([...])(value))分支内value会被收窄为所选变体的联合适合作为路由分发、事件过滤等场景的前置判断更复杂的按分支处理则交给match/matchOrElse。注意判别值的唯一性toTaggedUnion会在构造时对重复判别值含1与1抛出异常这是设计上的安全网避免cases映射被覆盖。小结Schema.toTaggedUnion(...)的本次修复.changeset patch解决了自定义判别键下isAnyOf类型收窄失效的问题类型签名不再硬编码_tag而是复用构造时传入的Tag泛型使类型层面与运行时行为完全对齐。对于使用_tag的默认场景TaggedUnion与既有isAnyOf签名不受任何影响对于使用自定义判别键的场景现在可以放心依赖isAnyOf的类型收窄能力编写类型安全的判别联合处理逻辑。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表