- 状态管理
- 前端
【免费下载链接】mobx-state-tree
Full-featured reactive state management without the boilerplate
UnionOptions<Types>是 mobx-state-tree 中types.union的配置接口,用于控制联合类型在快照校验与节点实例化时的匹配策略。本文以该接口为骨架,结合 union.ts 源码与 union.test.ts 测试用例,深入讲解eager与dispatcher两个可选配置项的语义、默认值、底层实现与实战用法。读完本文,你将能精确掌控联合类型在多候选匹配、歧义消除、复杂类型判别等场景下的行为,写出类型安全且行为可预期的 MST 模型。
一、UnionOptions 接口概览
UnionOptions定义于 src/types/utility-types/union.ts#L26-L39,在 src/index.ts#L72 中作为公开类型导出。它只有一个类型参数:
| 类型参数 | 约束 | 含义 |
|---|---|---|
Types | IAnyType[] | 参与联合的候选类型数组(IAnyType定义见 ianytype.md) |
接口本身只包含两个可选属性:
| 属性 | 类型 | 默认行为 | 说明 |
|---|---|---|---|
dispatcher | ITypeDispatcher<Types> | 不提供 | 接收输入快照、返回应使用的具体类型 |
eager | boolean | true | 是否采用"急切(eager)"校验 |
从源码构造函数可以看出默认值的落地方式:
options = { eager: true, dispatcher: undefined, ...options } this._dispatcher = options.dispatcher if (!options.eager) this._eager = false即:不传eager时等价于eager: true;dispatcher缺省为undefined。在非生产环境(devMode())下,types.union还会对第一个参数做运行时断言,要求必须是形如object { eager?: boolean, dispatcher?: Function }的普通对象(见 union.ts#L196-L208),并在 union.test.ts#L167-L175 中验证了误传裸函数会抛出 "expected object"。
二、eager:急切校验开关
eager决定联合类型在校验(validation)阶段的匹配粒度,接口注释给出的语义是:
当为
true时,采用第一个匹配的类型;否则会检查所有候选类型,只有当且仅当恰好一个类型匹配时,校验才通过。
底层实现:isValidSnapshot 的两种分支
在 union.ts#L129-L151 的isValidSnapshot中可以看到两种模式的完整逻辑:
const allErrors: IValidationError[][] = [] let applicableTypes = 0 for (let i = 0; i < this._types.length; i++) { const type = this._types[i] const errors = type.validate(value, context) if (errors.length === 0) { if (this._eager) return typeCheckSuccess() // eager:命中即返回 else applicableTypes++ // 非 eager:继续统计 } else { allErrors.push(errors) } } if (applicableTypes === 1) return typeCheckSuccess() // 恰好一个匹配 return typeCheckFailure(context, value, "No type is applicable for the union").concat( flattenTypeErrors(allErrors) )eager: true(默认):遍历候选类型时,只要遇到第一个validate无错误的类型就立即判定成功,不再检查后续类型。因此候选类型的声明顺序直接决定匹配结果。eager: false:必须遍历完所有候选类型并统计匹配数,applicableTypes === 1才通过校验;0 个或多个匹配都会失败,失败信息统一为 "No type is applicable for the union",并附带展平后的各类型错误明细。
测试用例印证
union.test.ts#L49-L56 展示了eager: false下"多个类型同时适用"会抛错的行为:
const PlaneNotEager = types.union({ eager: false }, Square, Box) expect(() => { PlaneNotEager.create({ width: 2, height: 2 }) // 同时符合 Square 与 Box }).toThrow(/Error while converting/)而默认eager: true下,顺序优先的语义由 union.test.ts#L116-L135 一组字面量测试直观体现:
const All = types.model("All", { type: types.string }) const Man = types.model("Man", { type: types.literal("M") }) // types.union(Man, Woman, All) 中 All 在最后,所以 "M" 先命中 Man // types.union(All, Man) 中 All 在最前,所以 "M" 被 All 抢走! expect(types.union(All, Man).create({ type: "M" })) // All.is(person) === true,Man.is(person) === false这提醒我们:使用默认eager时,更具体的类型必须放在更宽泛的类型之前,否则宽泛类型会"抢先"匹配(测试注释中的 "All grabbed everything!")。
三、dispatcher:显式指定匹配类型
dispatcher是ITypeDispatcher<Types>类型的函数,接口注释定义为"根据输入快照返回要使用的类型":
export type ITypeDispatcher<Types extends IAnyType[]> = ( snapshot: Types[number]["SnapshotType"] ) => Types[number]即它接收快照(SnapshotType),返回联合中的某一个类型。这个签名在 docs/API/index.md 的ITypeDispatcher条目中有完整记载。
底层实现:determineType 的优先顺序
在实例化(instantiate)、协调(reconcile)与校验(isValidSnapshot)三条路径上,dispatcher 都拥有最高优先级。以 union.ts#L108-L127 的determineType为例:
if (this._dispatcher) { return this._dispatcher(value) // 1. dispatcher 优先 } if (reconcileCurrentType) { if (reconcileCurrentType.is(value)) { return reconcileCurrentType // 2. 协调时优先复用当前节点类型(#1045) } return this._types.filter(t => t !== reconcileCurrentType).find(type => type.is(value)) } else { return this._types.find(type => type.is(value)) // 3. 默认按顺序找第一个匹配 }instantiate(创建):determineType(initialValue, undefined)直接采用 dispatcher 返回的类型来实例化节点;reconcile(快照协调):会传入current.getReconciliationType(),先尝试 dispatcher,再尝试复用当前节点类型(这是 union.test.ts#L187-L317 中 #1045 场景修复的关键),避免已有节点在applySnapshot时被错误切换类型;isValidSnapshot(校验):只要配置了 dispatcher,就直接委托this._dispatcher(value).validate(value, context),不再走 eager 的遍历逻辑。
因此,提供 dispatcher 后,eager配置对校验分支不生效——两者是"二选一"的匹配手段:dispatcher 显式指定,eager 控制自动匹配的粒度。
实战用法
union.test.ts#L137-L176 给出了一个经典场景——按快照内容判别奇偶类型:
const Num = types.union( { dispatcher: snapshot => (snapshot.value % 2 === 0 ? Even : Odd) }, Even, Odd ) expect(Num.create({ value: 3 }).isOdd()).toBe(true) // 奇数 → Odd expect(Num.create({ value: 4 }).isEven()).toBe(true) // 偶数 → Evenunion.test.ts#L31-L35 则展示了按属性存在性判别:
const DispatchPlane = types.union( { dispatcher: snapshot => (snapshot && "height" in snapshot ? Box : Square) }, Box, Square )复杂联合类型的坑与解法
联合类型在复杂模型上可能产生"幽灵匹配"。文档 more-tips.md 的 "Complex union types" 一节给出了反例:两个模型分别带foo: string[]与bar: number[],直接types.union(Foo, Bar)后,期望得到{ foo: ["test"], bar: [200] },实际却得到{ foo: ["test"], foo: [] }。两种修复方式:
方式一:dispatcher 显式判别
const FooBar = types.union( { dispatcher: snapshot => { if (snapshot.foo) return Foo return Bar } }, Foo, Bar )方式二:给基础模型加字面量标签
const Foo = types.model({ foo: types.array(types.string), type: types.literal("foo") }) const Bar = types.model({ bar: types.array(types.number), type: types.literal("bar") })字面量标签配合默认 eager 模式即可让联合类型精确区分,这也是 docs/overview/types.md#L51 中 "types.literal与union组合非常强大" 的原因。事实上types.enumeration本身就是"字符串字面量联合"的语法糖(见 union.ts 中的说明与 index.md#L2446)。
四、union 函数签名与 UnionOptions 的传参位置
UnionOptions作为types.union的第一个可选参数存在,函数有两个重载(union.ts#L175-L179):
export function union<Types extends IAnyType[]>(...types: Types): IUnionType<Types> export function union<Types extends IAnyType[]>( options: UnionOptions<Types>, ...types: Types ): IUnionType<Types>调用时有两种形式:
// 形式一:直接传候选类型 const Plane = types.union(Square, Box) // 形式二:先传 options,再传候选类型 const DispatchPlane = types.union( { dispatcher: snapshot => ..., eager: false }, Box, Square )实现中通过isType(optionsOrType)区分两种形态:第一个参数是类型则视为候选类型,否则作为 options 处理(union.ts#L191-L193)。返回类型为IUnionType<Types>,其类型层面等价于候选类型各维度的联合(CreationType / SnapshotType / TypeWithoutSTN 的_CustomCSProcessor规约,见 union.ts#L162-L173)。生成的名字形如"(Square | Box)",错误信息也会携带该描述。
五、选择建议:何时用 eager,何时用 dispatcher
结合 overview/types.md#L49 的官方概述与上述源码行为,可归纳出如下决策路径:
| 场景 | 推荐配置 | 原因 |
|---|---|---|
| 候选类型结构差异明显、可被快照形状区分(如字面量标签、互斥字段) | 默认eager: true,把更具体的类型放前面 | 无需额外代码,声明顺序即匹配优先级 |
候选类型存在重叠(如{width}模型与{width, height}模型),歧义必须被拒绝 | eager: false | 只有恰好一个类型匹配才通过,避免静默错配 |
| 候选类型靠形状无法区分,或需要业务逻辑(如奇偶、权限、版本号)决定类型 | 配置dispatcher | 显式、确定性地返回目标类型,同时跳过遍历开销 |
对既有节点做applySnapshot,希望尽量保持原节点类型 | 配置dispatcher或依赖 reconcile 的当前类型优先逻辑 | 见 #1045 的修复路径(union.ts#L118-L123) |
最后提醒一点:UnionOptions的两个属性都是可选的,但二者存在协作与互斥关系——提供dispatcher时匹配结果完全由其决定,eager仅作用于无 dispatcher 的自动匹配分支。理解这一点,就能在设计可空联合、可辨识联合与多态模型时做出精确且可预期的类型决策。
延伸阅读:完整的union函数文档见 docs/API/index.md 的union条目;IUnionType与ITypeDispatcher类型定义见 src/types/utility-types/union.ts;更多联合类型实战与调试建议见 docs/tips/more-tips.md。
- 状态管理
- 前端
【免费下载链接】mobx-state-tree
Full-featured reactive state management without the boilerplate
相关推荐
mobx-state-tree CustomTypeOptions 详解:用 types.custom 打造自定义序列化类型
mobx state tree CustomTypeOptions 详解:用 types.custom 打造自定义序列化类型 导读 CustomTypeOpti
状态管理前端终极指南:使用mobx-state-tree掌握复合、装饰与策略模式的状态模型设计
终极指南:使用mobx state tree掌握复合、装饰与策略模式的状态模型设计 mobx state tree是一个功能全面的响应式状态管理库,它通过简洁的
状态管理前端DeepChat 数据落盘位置权威指南:从主数据库到加密元数据、备份布局与快照导入规则
DeepChat 数据落盘位置权威指南:从主数据库到加密元数据、备份布局与快照导入规则 本文基于 DeepChat 仓库中供数据导入工具使用的参考文档 data
状态管理前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考