如何使用精确的通用类型制作 Typescript Reducer

How to Make a Typescript Reducer with Exact Generic Types

问题

我有两个减速器,它们对包含点的两个不同数组使用完全相同的逻辑。数组将只有一种类型的点;这些类型是 AB。我想创建一个 reducer,它接受一个类型为 A 或类型为 B 的点数组,然后是 returns 一个相同类型的数组。

当我尝试编译下面的代码时,出现此错误:

Argument of type 'Partial<A> | Partial<B>' is not assignable to parameter of type 'Partial<T>'.
  Type 'Partial<A>' is not assignable to type 'Partial<T>'.

我该如何解决这个问题?

有问题的代码

enum Category {
  A,
  B
}

interface A  {
  readonly category: Category.A;
}
interface B  {
  readonly category: Category.B;
}

Category = 

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) => {
        return updateObject(stpt, action.payload.newValues);
      });
    }

    default:
      return points;
  }
};

ArrayUtils.updateObject

供参考,这里是 updateObject 函数:

static updateObject = <T>(oldObject: T, newValues: Partial<T>) => {
    // Encapsulate the idea of passing a new object as the first parameter
    // to Object.assign to ensure we correctly copy data instead of mutating
    return Object.assign({}, oldObject, newValues);
};

updateItemInArray 函数

static updateItemInArray = <T>(array: T[], itemId: string, updateItemCallback: (item: T) => T): T[] => {
    const updatedItems = array.map((item) => {
      if (item.id !== itemId) {
        // Since we only want to update one item, preserve all others as they are now
        return item;
      }

      // Use the provided callback to create an updated item
      const updatedItem = updateItemCallback(item);
      return updatedItem;
    });

    return updatedItems;
};

编辑#1

这是我根据 Linda Paiste 的回答所做的最新尝试的 CodeSandbox link。此时仍然存在分配给类型 Partial<T>.

的问题

CodeSandbox Link

我开始尝试填写您的代码中缺失的部分,以便找出问题所在。这样做,很明显问题出在您的 Action 类型中(如@Alex Chashin 所建议)。

我知道 Actions 有一个 type 需要包含 POINT_UPDATED。它还有一个 payload。有效负载包括一个 id、一个 category 和一些 newValues.

type Actions = {
    type: typeof POINT_UPDATED;
    payload: {
        id: string;
        category: Category;
        newValues: ????;
    }
}

什么是newValues?根据updateObject的签名,我们知道应该是Partial<T>但是Actions不知道T是什么。

我猜你的代码使用了 newValues: A | B; 之类的东西,这给了我你发布的错误。这不够具体。仅仅知道我们的新值是“AB”是不够的。 updateObject 说如果 TA 那么 newValues 必须是 A 并且如果 TB 那么 newValues 必须是 B.

因此您的 Actions 需要是通用类型。

type Actions<T> = {
    type: typeof POINT_UPDATED;
    payload: {
        id: string;
        category: Category;
        newValues: Partial<T>;
    }
}

const genericReducer = <T extends A | B>(
  state: MyState,
  action: Actions<T>,
  points: T[],
  category: Category
): T[]
...

我在尝试访问 item.id 时在您的 updateItemInArray 上看到一个错误:

Property 'id' does not exist on type 'T'

你需要细化 T 的类型,这样它就知道 id 属性:

const updateItemInArray = <T extends {id: string}>(

这样做会导致您的 genericReducer 出现新的错误,因为您已经说过 T 必须有一个 category 但您没有说它必须有一个 id.我们可以解决这个问题:

const genericReducer = <T extends (A | B) & {id: string}>(

相同
const genericReducer = <T extends {id: string; category: Category}>(

尽管实际上看起来您从来没有看过 T[] 点上的 category。所以你真正需要的是:

const genericReducer = <T extends {id: string}>(

TypeScript Playground Link


编辑:

您在修改后的代码中遇到的错误真的很愚蠢。 技术上 T extends {elevation: number} 的类型。因此,它可能需要更具体的类型版本,例如 {elevation: 99}。在那种情况下,{elevation: number} 将不能分配给 Partial<{elevation: 99}>.

至少我认为是问题所在。但是我的第一个修复程序没有用。我试图细化 Actions 的类型,说 elevation 属性 必须与 T.

中的匹配
type Payload<T extends A_or_B> = {
  [ActionTypes.FETCH_SUCCEEDED]: {
    id: string;
    elevation: T['elevation'];
    timestamp: number;
  };
...

但我仍然遇到错误,所以现在我完全糊涂了。

Argument of type { elevation: T["elevation"]; } is not assignable to parameter of type Partial<T>

我真的无法解释那个。

您可能不得不做出断言。 (不要为上面的修复而烦恼)。

return updateObject<{elevation: number}>(stpt, {
   elevation: action.payload.elevation,
}) as T;

我们通过将该函数的通用 T 设置为 {elevation: number} 来避免 updateObject 函数中的错误。 stpt(类型 T)和 {elevation: number} 都可分配给该类型。

但是现在 updateObject 函数的 return 类型是 {elevation: number} 而不是 T。所以我们需要断言 as T 来改变类型。