Effect 4.xTaggedUnion.match类型修复:借助 Unify 协议统一模式匹配分支返回类型
【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect
本文围绕 Effect 仓库中一条 patch 级变更记录.changeset/pre/fix-tagged-union-match-unify.md展开,它修复了Schema.TaggedUnion/Schema.toTaggedUnion提供的match方法在类型层面的返回值推断问题:让每个分支可以返回互不相同的 Effect 类型,并通过 Effect 的Unify协议在编译期自动合并为可继续链式调用的统一类型。读完本文,你将理解该修复的动机、Unify类型协议的底层原理、match/matchOrElse的类型签名变化,以及它在源码与测试中的具体落点。
一、这条 changeset 说了什么
changeset 是 Effect 仓库(基于 Changesets 工具)记录版本变更的载体,位于.changeset/pre/目录下。该文件内容如下:
--- "effect": patch --- Fix `TaggedUnion.match` to use `Unify` for return types, allowing branches to return distinct Effect types that are properly merged.逐项解读:
"effect": patch:变更影响effect主包,级别为 patch(补丁)。这意味着它是向后兼容的缺陷修复,不会破坏现有 API,将在下一个 patch 版本中随主包一起发布。- 修复对象:
TaggedUnion.match,即Schema.TaggedUnion(...)与Schema.Union([...]).pipe(Schema.toTaggedUnion(...))附带的方法之一。 - 修复内容:
match的返回值类型改用Unify进行规范化,使各分支可以返回不同的 Effect 类型(例如错误类型不同的Effect),并在类型层面被"正确合并"(properly merged)。
这条记录本身只有两行正文,但指向的功能——TaggedUnion.match与Unify协议——在仓库源码中有完整的实现,下面结合源码逐层展开。
二、背景:TaggedUnion与toTaggedUnion是什么
Schema.TaggedUnion是 Effect Schema 模块中用于构建带判别字段(discriminant)的联合结构的构造器。典型用法(出自 Schema.ts 的 JSDoc 示例):
import { Schema } from "effect" const Shape = Schema.TaggedUnion({ Circle: { radius: Schema.Number }, Rectangle: { width: Schema.Number, height: Schema.Number } }) // Shape 携带 cases、guards、isAnyOf、match、matchOrElse 等实用方法如果已有现成的Schema.Union,也可以用Schema.toTaggedUnion("_tag")为其增强同样的方法集(toTaggedUnion 的 JSDoc):
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"从 toTaggedUnion 的运行时实现可以看到,match的运行时逻辑非常直接:读取value[tag]判别值,用Object.hasOwn找到对应的处理器函数并调用(同时支持match(value, cases)与柯里化match(cases)(value)两种调用形式);matchOrElse则在找不到处理器时回退到orElse函数。因此这次修复是纯类型层面的改动,不涉及运行时行为变化。
三、修复核心:match的返回类型改用Unify
3.1 修复前的类型签名(问题所在)
在修复之前,TaggedUnionUtils中match的返回类型是分支返回类型的裸联合。考虑如下场景:
import { Schema, Effect } from "effect" const Event = Schema.TaggedUnion({ Success: { data: Schema.String }, Failure: { error: Schema.String } }) const program = Event.match({ Success: ({ data }) => Effect.succeed(data), // Effect<never, never, string> Failure: ({ error }) => Effect.fail(error) // Effect<never, string, never> })若返回类型不做规范化,program会被推断为Effect<never, never, string> | Effect<never, string, never>。这种"联合的 Effect"在随后调用.pipe(...)等链式 API 时会非常难用:TypeScript 只能对联合的公共成员调用方法,且R/E类型参数无法自然地归并,这正是 changeset 所说的"无法被正确合并"的痛点。
3.2 修复后的类型签名(源码证据)
修复后的定义位于 packages/effect/src/Schema.ts 的TaggedUnionUtils类型中,match的两个重载均以Unify<R>收尾:
readonly match: { < Cases extends { [M in Flattened[number] as M["Type"][Tag]]: (value: M["Type"]) => any } >( value: Members[number]["Type"], cases: Cases ): Cases[keyof Cases] extends (value: any) => infer R ? Unify<R> : never < Cases extends { [M in Flattened[number] as M["Type"][Tag]]: (value: M["Type"]) => any } >( cases: Cases ): (value: Members[number]["Type"]) => Cases[keyof Cases] extends (value: any) => infer R ? Unify<R> : never }两个重载分别对应match(value, cases)与柯里化match(cases)(value),二者都先把所有分支的返回类型收拢为联合R(Cases[keyof Cases] extends (value: any) => infer R ? R : never),再交给Unify<R>规范化。这样上例中program会被推断为单个Effect<never, string, string>,可以直接继续链式操作。
3.3matchOrElse同样被 Unify 覆盖
改动不仅限于match。matchOrElse的返回类型通过MatchOrElseResult计算,同样显式包裹了Unify(Schema.ts):
type MatchCasesResult<Cases> = { [K in keyof Cases]-?: NonNullable<Cases[K]> extends (...args: Array<any>) => infer R ? R : never }[keyof Cases] type MatchOrElseResult<Cases, OrElse extends (...args: Array<any>) => any> = Unify< MatchCasesResult<Cases> | ReturnType<OrElse> >可见联合的"各分支返回类型 + orElse 回退类型"整体都被Unify规范化,保证matchOrElse在分支返回不同 Effect 类型时也能推断出可用的统一类型。
四、Unify协议:底层原理
Unify是 Effect 的类型级统一协议,定义在 packages/effect/src/Unify.ts(自 2.0.0 起存在),由三个 unique symbol 组成:
unifySymbol:描述某个协议类型在统一(widening)时应产生什么公开类型;typeSymbol:存放参与统一的源类型信息;ignoreSymbol:列出统一时应被忽略的辅助协议条目。
核心类型Unify<A>(Unify.ts)对输入A做三件事:
- 提取实现了协议(携带
typeSymbol/unifySymbol)的成员,取出其unifySymbol声明的目标类型; - 保留那些没有匹配到目标类型的协议成员本身(
FilterInUnmatched); - 原样保留未实现协议的普通类型(
FilterOut)。
最终结果等价于把联合成员各自"展开"后再重组。运行时配套的Unify.unify则是一个恒等函数(Unify.ts),只改变推断出的类型,不产生任何运行时开销。
Effect、Option、Result、Stream、Layer、Match等数据类型都实现了该协议(Unify.ts 的模块说明明确列出了这些受益方),因此当TaggedUnion.match的分支返回Effect.succeed(...)/Effect.fail(...)时,Unify能把Effect<never, never, string> | Effect<never, string, never>规范化为统一的Effect<never, string, string>——这就是 changeset 中 "branches to return distinct Effect types that are properly merged" 的机制来源。
五、同类模式:Match模块的返回值早已使用 Unify
这次修复让TaggedUnion.match与 Effect 的另一套模式匹配 API——Match模块——在返回类型处理上保持一致。Match对外 API 中,Match.valueTags与Match.typeTags的返回类型均写为Unify<ReturnType<P[keyof P]>>(packages/effect/src/Match.ts 与 L518),其内部实现位于 packages/effect/src/internal/matcher.ts(如 typeTags):
return (input: I): Unify<ReturnType<P[keyof P]>> => match(input)同样地,discriminatorsExhaustive、tagsExhaustive、orElse、orElseAbsurd、result、option、exhaustive等内部函数的返回类型也全部以Unify<...>收尾(matcher.ts)。可以推断:"分支返回类型先取联合、再经Unify规范化"是 Effect 中所有模式匹配型 API 的统一类型约定,本次 changeset 正是把TaggedUnion.match对齐到这一约定上。
六、测试佐证
仓库测试 packages/effect/test/schema/Schema.test.ts 对toTaggedUnion与TaggedUnion的行为覆盖相当完整,可作为本修复所依赖运行时语义的验证依据:
- 判别值收集:
schema.discriminants按成员扁平化顺序返回(支持字符串、数字、UniqueSymbol,如["A", b, 1, "D"]); - 重复判别值抛错:对相同字面量(包括
1与"1"这类易混情况)抛出Duplicate discriminant: ...(Schema.test.ts); - 空联合与特殊键:空
Union得到空discriminants;__proto__作为判别值时cases/guards仍能通过Object.hasOwn正确命中(Schema.test.ts); - match 双调用形式:直接调用
schema.match(value, cases)与pipe(value, schema.match(cases))结果一致(Schema.test.ts); - matchOrElse 回退:未命中分支时正确返回
orElse结果(如() => "fallback"); - 多标签与类成员:支持以
type等非_tag字段作为判别键,也支持Schema.Class成员。
七、版本与升级影响
- 变更级别:patch(向后兼容的类型修复),随
effect包下一个补丁版本发布;对现有使用TaggedUnion.match/matchOrElse的代码,运行时行为不变。 - 类型收益:升级后,分支返回不同类型
Effect的代码将获得更精确、可链式调用的推断类型;若此前依赖"裸联合"推断(极少见),需注意推断结果的收窄。 - 适用范围:本修复覆盖
Schema.TaggedUnion(...)与Schema.Union([...]).pipe(Schema.toTaggedUnion(tag))两条路径生成的match/matchOrElse(二者最终都由toTaggedUnion的同一套TaggedUnionUtils类型提供签名,见 Schema.ts)。
结语
一条两行的 changeset 背后,是一次将TaggedUnion.match的对齐进 Effect 类型系统统一约定的修复:Unify协议把"各分支返回不同 Effect 类型"的联合结果在编译期正确合并,让基于判别联合的模式匹配可以无缝衔接后续的 Effect 链式编程。理解Unify的unifySymbol/typeSymbol/ignoreSymbol协议与TaggedUnion的运行时机制,不仅能解释这条变更记录,也能让你在自定义数据类型时主动接入这一协议,享受同样的类型推断红利。
【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考