ARTICLE DETAIL

资讯详情

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

MobX-State-Tree 自动生成属性 Setter 动作:用 `setProp` 泛型 Action 消除样板代码

MobX-State-Tree 自动生成属性 Setter 动作:用 `setProp` 泛型 Action 消除样板代码 状态管理前端【免费下载链接】mobx-state-treeFull-featured reactive state management without the boilerplate项目地址https://gitcode.com/gh_mirrors/mo/mobx-state-tree点击查看免费下载导读在 MobX-State-TreeMST中修改模型属性必须通过 action 完成而最常见的做法是为每个属性手写一个 setter随着模型字段增多会积累大量重复样板代码。本文基于官方 Recipes 文档《Auto-Generated Property Setter Actions》整理并深化给出两种落地方案在模型内部直接声明一个泛型setPropaction或提取为可跨模型复用的withSetPropActionhelper并结合仓库源码说明其类型推导SnapshotIn、IStateTreeNode与 MST action 机制src/types/complex-types/model.ts的底层原理。读完本文你将能在任意 MST 模型中用一行.actions(withSetPropAction)获得类型安全的批量属性写入能力。一、问题背景逐属性手写 setter 的样板困局MST 强制要求状态变更必须发生在 action 内因此常规做法是一个属性配一个 setter。以一个只有两个字段的模型为例手写版本是这样的import { types } from mobx-state-tree const UserModel types .model(User, { name: types.string, age: types.number }) .actions((self) ({ setName(newName: string) { self.name newName }, setAge(newAge: number) { self.age newAge } }))这套写法的痛点是显而易见的随字段数线性膨胀每新增一个属性就要补一个 setter10 个字段就是 10 个几乎一模一样的函数稀释核心逻辑setName、setAge这类机械代码占据大量篇幅真正有业务逻辑的 action 反而被淹没命名难统一团队中可能出现setName、updateAge、changeEmail等风格不一的命名加大维护成本。这类样板问题正是本 Recipes 文档要解决的用一个泛型 action 或一个可复用 helper统一按属性名写值这件事。二、方案一在模型内声明通用泛型setPropaction如果只想在一个模型内解决样板问题可以直接在actions块中声明一个泛型方法利用SnapshotIntypeof self推导出当前模型全部属性的键与其值类型import { types, SnapshotIn } from mobx-state-tree const UserModel types .model(User, { name: types.string, age: types.number }) .actions((self) ({ setPropK extends keyof SnapshotIntypeof self, V extends SnapshotIntypeof self[K]( field: K, newValue: V ) { self[field] newValue } })) const user UserModel.create({ name: Jamon, age: 40 }) user.setProp(name, Joe) // 通过类型检查 // 传入错误类型会被 TypeScript 拦截这正是我们想要的 user.setProp(age, shouldnt work) // 类型错误age 需要 number这里的关键在于SnapshotIn类型。查看仓库源码src/core/type/type.ts可以看到它的定义export type SnapshotInT T extends { [$type]: undefined; CreationType: any } ? T[CreationType] : T extends IStateTreeNodeinfer IT ? IT[CreationType] : TSnapshotIntypeof self会把self的状态树节点类型还原为其创建快照类型也就是{ name: string; age: number }这样的普通对象结构。于是K extends keyof SnapshotIntypeof self把field约束为name | ageV extends SnapshotIntypeof self[K]让newValue与所选中字段的类型精确对齐name对应string、age对应number由于它仍是actions块内的方法写入操作天然运行在 MST action 上下文中享受 action 的全部语义中间件、patch 记录、时间旅行等。局限这种方式每个模型都要复制粘贴一遍泛型签名跨模型复用性差。因此文档进一步给出了提取 helper 的推荐做法。三、方案二提取可复用的withSetPropActionhelper把泛型逻辑抽成一个独立的 helper 函数所有模型通过.actions(withSetPropAction)一行接入。这是本 Recipes 的核心成果最初由 Infinite Red 社区提出以下为其完整实现import { IStateTreeNode, SnapshotIn } from mobx-state-tree // 这个自定义类型让 TS 知道返回函数可以修改哪些属性 // 它排除了 actions 和 views但仍能正确推断模型属性用于自动补全与类型安全。 type OnlyPropertiesT { [K in keyof SnapshotInT]: K extends keyof T ? T[K] : never } /** * 把这个 helper 放进模型的 actions() 块中紧跟在 props 之后 * 它允许你直接按属性名写入值既保留类型安全又始终运行在 action 上下文中。 * 这能省去大量只更新一个 prop的重复 setter 动作。 * * 用法示例 * * const UserModel types.model(User) * .props({ * name: types.string, * age: types.number * }) * .actions(withSetPropAction) * * const user UserModel.create({ name: Jamon, age: 40 }) * * user.setProp(name, John) // 无类型错误 * user.setProp(age, 30) // 无类型错误 * user.setProp(age, 30) // 类型错误 —— 必须是 number */ export const withSetPropAction T extends IStateTreeNode(mstInstance: T) ({ setPropK extends keyof OnlyPropertiesT, V extends SnapshotInT[K](field: K, newValue: V) { ;(mstInstance as T OnlyPropertiesT)[field] newValue } })3.1 逐行拆解实现原理泛型参数T extends IStateTreeNodeIStateTreeNode是 MST 中所有状态树节点实例的共同接口。查看源码src/core/node/node-utils.ts它通过一个unique symbol携带类型信息$stateTreeNodeTypeMST 的SnapshotIn、SnapshotOut、Instance等类型工具正是依赖这层隐式标记完成类型级解包。约束到IStateTreeNode保证了只有真正的模型实例能传入从而拿到其快照类型。OnlyPropertiesT映射类型遍历SnapshotInT的所有键只保留在T实例类型中真实存在且可赋值的键。由于 views如get lowercaseName()和 actions如setName并不存在于快照结构中它们会被SnapshotInT天然排除——这正对应文档注释中排除 actions 和 views只保留模型属性的目标。setProp的签名K extends keyof OnlyPropertiesT限定field必须是可写属性名V extends SnapshotInT[K]把newValue精确绑定到该属性的快照值类型。写入断言(mstInstance as T OnlyPropertiesT)[field] newValue运行时mstInstance就是真实的模型实例self上的普通属性可以直接赋值类型层面的断言只是为了说服编译器允许这种按索引的泛型写入。前面的分号是为了防止 ASI自动分号插入问题避免与上一行合并解析。返回对象即 action 集合withSetPropAction返回{ setProp }这样一个普通对象。对照 MST 模型实现src/types/complex-types/model.ts.actions(fn)内部会调用fn(self)并用instantiateActions把返回对象中的每个函数包装为 action invoker 挂载到节点上src/types/complex-types/model.ts。因此setProp与手写 setter 在 MST 语义上完全等价同样是 action、同样可被中间件与 devtools 追踪。四、在模型中接入 helper完整实战示例下面是一个同时包含属性、view 和手写 action 的完整模型展示withSetPropAction与它们共存的方式import { t } from mobx-state-tree import { withSetPropAction } from ./withSetPropAction const Person t .model(Person, { name: t.string }) .views((self) ({ get lowercaseName() { return self.name.toLowerCase() } })) .actions((self) ({ setName(name: string) { self.name name } })) .actions(withSetPropAction) const you Person.create({ name: your name }) you.setProp(name, Another Name) // 正常写入运行在 action 上下文几点工程实践提示放置顺序建议把withSetPropAction放在.props()或.model()之后、其他业务 actions 之后的独立.actions()块中如文档示例所示。MST 的actions可以链式多次调用每次调用都会在既有类型上叠加新的 action 集合见 src/types/complex-types/model.ts 的类型签名与 src/types/complex-types/model.ts 的实现因此 helper 与手写 action 互不干扰。导入方式withSetPropAction.ts文件由你自行创建并导出该函数模型文件只需import { withSetPropAction } from ./withSetPropAction。命名冲突setProp是一个通用名若你的模型恰好已有名为setProp的属性或 actionMST 会在instantiateActions阶段抛出错误——源码中明确检查了action 与属性同名${name} is a property and cannot be declared as an action见 src/types/complex-types/model.ts。接入前请确认命名不冲突。t与types等价示例中使用了t别名导入MST 从早期版本起就支持import { t } from mobx-state-tree与types完全等价可按团队风格任选。五、类型安全验证哪些错误会被 TypeScript 拦下withSetPropAction的核心价值在于该拦的拦、该放行的放行。文档用一个try/catch块系统演示了四类错误场景运行到错误代码时会触发运行时错误因此用ts-expect-error标注以证明编译期拦截// 以下调用都伴随运行时错误此处仅为演示 TS 对 withSetPropAction 的支持。 try { // ts-expect-error - 类型不对name 是 string传入 number 应报错。 you.setProp(name, 123) // ts-expect-error - nah 不是 Person 的任何属性应报错。 you.setProp(nah, 123) // ts-expect-error - 不能像写属性一样写 view。 you.setProp(lowercaseName, your name) // ts-expect-error - 不能像写属性一样写 action。 you.setProp(setName, your name) } catch (e) { console.error(e) }对照前面的类型推导可以逐条验证编译器行为调用TypeScript 行为原因setProp(name, Another Name)✅ 通过name是属性Another Name匹配stringsetProp(name, 123)❌ 报错V extends SnapshotInT[name] string123不匹配setProp(nah, 123)❌ 报错nah不在keyof OnlyPropertiesT中setProp(lowercaseName, ...)❌ 报错view 不出现在SnapshotInT中被OnlyProperties排除setProp(setName, ...)❌ 报错action 同样不在快照键集合中不可作为写入目标也就是说可写字段白名单 模型属性值类型 该属性在快照中的类型。views 和 actions 被系统性地排除在写入范围之外既保证了运行时安全不会误改计算值或方法又让 IDE 自动补全只提示真实可写字段。仓库测试中也存在与此模式一致的实践例如tests/core/snapshotProcessor.test.ts 中通过self.prop prop的方式在 action 内写入属性并断言更新结果佐证了action 内直接赋值属性是 MST 支持的常规操作。六、适用场景与边界推荐使用setProp/withSetPropAction的场景模型以纯数据存储为主字段多、setter 逻辑机械重复需要快速搭建可写的数据对象如表单状态、配置项集合希望获得一致、可自动补全的写入 API减少命名分歧。仍建议手写专用 action 的场景写入伴随额外业务逻辑如校验、联动更新多个字段、记录日志等——此时应显式命名如updateProfile语义更清晰需要暴露受限的写接口给外部只允许改name不允许改age泛型setProp会把全部属性暴露出来对可读性要求极高、字段极少的简单模型手写 setter 的成本可以忽略。一个重要的类型细节文档在泛型写法中使用的是SnapshotIntypeof self而非Instancetypeof self。两者区别在于——SnapshotIn是输入快照类型{ name: string; age: number }Instance是节点实例类型带$treenode等内部标记。对写属性这件事快照类型更贴合赋值场景。仓库源码中SnapshotOrInstance类型src/core/type/type.ts正是为setter 场景准备的联合类型若你需要在 setter 中同时接受快照与实例作为入参可参考其注释中的用法。七、总结本 Recipes 提供了一条少写代码而不失类型安全的 MST 建模路径最朴素的方案是每属性手写 setter直观但样板多、随模型膨胀而失控模型内泛型setProp用SnapshotIntypeof self一把梭解决单个模型的样板问题提取withSetPropActionhelperT extends IStateTreeNodeOnlyPropertiesT映射类型让所有模型一行接入且自动排除 views/actions、精确约束值类型——这是文档推荐的最终形态。从源码视角看SnapshotIn借助IStateTreeNode的类型标记完成实例 → 快照结构的解包src/core/type/type.ts、src/core/node/node-utils.ts而.actions()链式叠加机制src/types/complex-types/model.ts保证了 helper 产出的setProp与手写 action 在中间件、patch、devtools 层面完全同权。这正是类型安全 降低样板 保持 MST action 语义三者兼得的实现基础。赞分享状态管理前端【免费下载链接】mobx-state-treeFull-featured reactive state management without the boilerplate项目地址https://gitcode.com/gh_mirrors/mo/mobx-state-tree点击查看免费下载相关推荐Unpackerr常见问题解决从安装到运行的10个实用技巧Unpackerr常见问题解决从安装到运行的10个实用技巧 Unpackerr是一款专为Starr应用如Radarr、Sonarr、Lidarr和Reada上一篇ComfyUI Essentials为什么这是每个AI绘画创作者必备的终极工具包下一篇终极量化交易学习指南从零掌握Python金融编程的完整路径创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表