You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 17:06:50