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

如何创建带有精确泛型类型的TypeScript Reducer并解决类型不兼容报错

解决TypeScript泛型Reducer中Partial类型不兼容问题

问题根源

你遇到的报错本质是联合类型的Partial推导逻辑导致的:当泛型T是A | B时,Partial<T>实际上等于Partial<A> & Partial<B>(因为联合类型的Partial要求属性对所有联合成员都可选),但你的action.payload.newValues是Partial<A> | Partial<B>,这两个类型无法直接赋值——Partial<A>并不满足Partial<A> & Partial<B>的约束(缺少B的可选属性),反之亦然。

另外,当前的泛型约束T extends A | B没有把T和传入的category参数绑定起来,TypeScript无法确定points的类型和action.payload的类型是否匹配,这加剧了类型不兼容问题。


修复方案

方案1:绑定泛型与Category的关联

通过泛型约束让T的category属性和传入的category参数严格对应,让TypeScript能精确推导类型:

enum Category { A, B }

interface A {
  readonly category: Category.A;
  id: string; // 补充updateItemInArray需要的id属性
}

interface B {
  readonly category: Category.B;
  id: string;
}

// 定义映射类型,让Category对应具体的Point类型
type PointByCategory<C extends Category> = 
  C extends Category.A ? A : B;

const genericReducer = <C extends Category>(
  state: MyState,
  action: Actions,
  points: PointByCategory<C>[],
  category: C
): PointByCategory<C>[] => {
  switch (action.type) {
    case POINT_UPDATED: {
      if (action.payload.category !== category) return points;
      // 用类型断言告诉TS:当前newValues和stpt的类型匹配
      return updateItemInArray(points, action.payload.id, (stpt) => {
        return updateObject(stpt, action.payload.newValues as Partial<PointByCategory<C>>);
      });
    }
    default:
      return points;
  }
};

方案2:优化updateObject的类型定义

如果不想修改泛型结构,可以调整updateObject的类型,让它直接基于传入的oldObject推导newValues的类型,这样更灵活:

class ArrayUtils {
  static updateObject = <T>(oldObject: T, newValues: Partial<typeof oldObject>) => {
    return Object.assign({}, oldObject, newValues);
  };

  static updateItemInArray = <T>(array: T[], itemId: string, updateItemCallback: (item: T) => T): T[] => {
    return array.map(item => item.id !== itemId ? item : updateItemCallback(item));
  };
}

然后在genericReducer中,利用类型守卫让TypeScript明确stpt的类型:

const genericReducer = <T extends A | B>(
  state: MyState,
  action: Actions,
  points: T[],
  category: Category
): T[] => {
  switch (action.type) {
    case POINT_UPDATED: {
      if (action.payload.category !== category) return points;
      return updateItemInArray(points, action.payload.id, (stpt) => {
        // 通过类型守卫匹配stpt和newValues的类型
        if (stpt.category === Category.A) {
          return updateObject(stpt, action.payload.newValues as Partial<A>);
        } else {
          return updateObject(stpt, action.payload.newValues as Partial<B>);
        }
      });
    }
    default:
      return points;
  }
};

方案3:让Actions类型与泛型关联(最严谨)

如果你的Actions类型可以泛型化,最好让POINT_UPDATED的payload类型和T绑定,从根源上避免类型不匹配:

type Actions<C extends Category = Category> = 
  | { type: 'POINT_UPDATED'; payload: { category: C; id: string; newValues: Partial<PointByCategory<C>> } }
  | /* 其他action类型 */;

const genericReducer = <C extends Category>(
  state: MyState,
  action: Actions<C>,
  points: PointByCategory<C>[],
  category: C
): PointByCategory<C>[] => {
  switch (action.type) {
    case POINT_UPDATED: {
      if (action.payload.category !== category) return points;
      // 此时newValues类型自动匹配Partial<PointByCategory<C>>,无需断言
      return updateItemInArray(points, action.payload.id, (stpt) => {
        return updateObject(stpt, action.payload.newValues);
      });
    }
    default:
      return points;
  }
};

内容的提问来源于stack exchange,提问作者RouteMapper

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 13:17:37