使用useImmerReducer时自定义Mappable类被转回Map的问题排查
问题:useImmerReducer中自定义Map子类Mappable被自动转回Map类型
问题描述
首次在React应用中使用useImmerReducer时遇到异常:自定义了继承自Map的Mappable类(已开启Immer支持),但状态中该类的实例会在某些阶段自动转回原生Map类型,导致调用Mappable自定义的.map()方法时出错,且state.checked的类型不稳定(时而为Mappable,时而为Map)。
自定义Mappable类代码
import { immerable } from "immer"; type KeyValueOf<T, K extends {}> = keyof T; export default class Mappable<K, V> extends Map { [immerable] = true; constructor(); constructor(entries: readonly (readonly [K, V])[]); constructor(entries: readonly V[], key: KeyValueOf<V, string>); constructor( entries?: readonly (readonly [K, V])[] | readonly V[], key?: keyof V ) { let superEntries: Map<V[keyof V], V> | readonly (readonly [K, V])[]; if (key) { superEntries = new Map( (entries as readonly V[]).map((entry: V) => [entry[key], entry]) ); } else if (entries) { superEntries = entries as readonly (readonly [K, V])[]; } else { superEntries = new Map(); } // @ts-ignore super(superEntries); } map = <T>(predicate: (key: K, value: V) => T): Mappable<K, T> => { let map: Mappable<K, T> = new Mappable(); super.forEach((value: V, key: K) => { map.set(key, predicate(key, value)); }); return map; }; }
状态定义与相关函数
export interface SelectorState { columns: SelectorColumnData[]; checked: Mappable<ItemId, ItemCheckedStateTree>; } export const columnCheckedState = ( state: SelectorState, columnIndex: number ): Mappable<ItemId, CheckedState> => { console.log("columnCheckedState", state); return state.checked.map( (id, item: ItemCheckedStateTree) => item.checkedState ); };
可能的原因及解决方案
- Immer对自定义集合子类的默认处理限制
Immer对原生Map/Set的克隆逻辑默认生成原生实例,即便标记了immerable,也不会自动识别并克隆自定义子类。需为Mappable添加Immer专属的克隆方法,明确告诉Immer如何复制该类实例:
// 在Mappable类中添加 [Symbol.for('immer.clone')]() { const cloned = new Mappable<K, V>(); this.forEach((value, key) => cloned.set(key, value)); return cloned; }
- 构造函数中的类型隐患
代码中// @ts-ignore隐藏了类型不匹配问题:super()接受的是可迭代条目数组,而非Map实例。当传入key时,superEntries被赋值为Map,导致构造逻辑异常,可能在Immer处理时被降级为原生Map。修复构造函数:
constructor( entries?: readonly (readonly [K, V])[] | readonly V[], key?: keyof V ) { let superEntries: readonly (readonly [K, V])[]; if (key) { // 直接生成条目数组,而非Map实例 superEntries = (entries as readonly V[]).map(entry => [entry[key], entry]) as readonly (readonly [K, V])[]; } else if (entries) { superEntries = entries as readonly (readonly [K, V])[]; } else { superEntries = []; } super(superEntries); }
Reducer中的状态更新方式错误
如果在reducer中直接将checked赋值为原生Map实例(比如new Map(...)),或使用Immer不兼容的修改方式,会导致类型转换。确保更新时始终生成Mappable实例,而非原生Map。初始状态类型不匹配
确认初始状态中的checked是Mappable实例,而非原生Map。若初始状态用了Map,Immer后续的状态更新会延续原生类型,导致类型不稳定。
内容的提问来源于stack exchange,提问作者serlingpa
相关产品推荐
相关产品推荐

