Как создать редуктор машинописи с точными универсальными типами

#typescript #generics #reducers

Вопрос:

Проблема

У меня есть два редуктора, которые используют одну и ту же логику для двух разных массивов, содержащих точки. Массив будет содержать только точки одного типа; этими типами являются A и B . Я хочу создать редуктор, который принимает массив точек типа A или типа B , а затем возвращает массив того же типа.

Когда я пытаюсь скомпилировать приведенный ниже код, я получаю эту ошибку:

 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

Вот ссылка CodeSandbox на мою последнюю попытку, основанную на ответе Линды Пейсте. В настоящее время все еще существует проблема с назначением типа Partial<T> .

Ссылка на CodeSandbox

Комментарии:

1. Проблема, скорее всего, в вашей updateItemInArray функции.

2. Пожалуйста, приведите воспроизводимый пример

3. Пожалуйста, укажите определение типа для Actions . Я почти уверен, что это причина проблемы, потому action.payload.newValues что имеет тип A | B и updateObject ожидает, что тип Partial<T> где T extends A | B . Если T extends A | B это не означает, что A | B это может быть присвоено ему

4. @капитан-йоссариан, пожалуйста, смотрите Правку №1 для ссылки на игровую площадку TS.

5. @капитан-йоссариан, я не сомневаюсь, что Линда сможет. Я довольно новичок в TS, и я поражен тем, как она заполнила пробелы. Я должен был просто предоставить пример CodeSandbox со всем кодом с самого начала.

Ответ №1:

Я начал пытаться заполнить недостающие части вашего кода, чтобы найти, в чем проблема. Сделав это, стало очевидно, что проблема в вашем Action типе (как предположил @Alex Chashin).

Я знаю, что в этом Actions есть type то, что нужно включить POINT_UPDATED . В нем также есть payload . Полезная нагрузка включает в себя an id , a category и некоторые newValues другие .

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

Что такое newValues ? Основываясь на подписи updateObject , мы знаем, что так и должно быть Partial<T> . Но Actions не знает, что T это такое.

Я предполагаю , что ваш код использует что-то вроде newValues: A | B; , что дает мне ошибку, которую вы опубликовали. Это недостаточно конкретно. Недостаточно знать, что наши новые ценности-это « A или B «. updateObject говорит, что если T есть A , то newValues должно быть A , а если T есть B , то должны быть 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[]
...
 

Я вижу ошибку на вашем updateItemInArray при попытке доступа item.id :

Свойство «id» не существует для типа «T»

Вам нужно уточнить тип T такого, который он знает о свойстве id:

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

Это приводит к новым ошибкам в вашем genericReducer , потому что вы сказали, что T это должно быть category , но вы не сказали, что это должно быть id . Мы можем исправить это с помощью:

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

что то же самое, что

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

Хотя на самом деле кажется, что вы никогда category не смотрите на свои точки T[] зрения . Так что все, что вам действительно нужно, это:

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

Ссылка на игровую площадку для машинописи


Редактировать:

Ошибка, которую вы получаете в своем пересмотренном коде, честно говоря, действительно глупая. Технически T extends тип с. {elevation: number} Таким образом, есть вероятность, что для этого может потребоваться более конкретная версия типа, например {elevation: 99} . В этом конкретном случае не было бы присвоения . {elevation: number} Partial<{elevation: 99}>

По крайней мере, я думал, что в этом и была проблема. Однако мое первое исправление не сработало. Я попытался уточнить тип Actions , чтобы сказать, что elevation свойство должно соответствовать свойству from T .

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

Но я все еще получаю ошибку, так что теперь я совершенно сбит с толку.

Аргумент типа { elevation: T["elevation"]; } не может быть присвоен параметру типа Partial<T>

Я действительно не могу этого объяснить.

Возможно, вместо этого вам придется сделать утверждение. (Не беспокойтесь об исправлении выше).

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

Мы избегаем ошибок внутри updateObject функции, устанавливая значение generic T этой функции {elevation: number} равным . Оба stpt (типа T ) и {elevation: number} могут быть назначены этому типу.

Но теперь возвращаемый тип updateObject функции {elevation: number} вместо T . Поэтому нам нужно утверждать as T , чтобы изменить тип.

Комментарии:

1. Хорошее объяснение, шаг за шагом

2. @Linda Paise Спасибо вам! Я больше не вижу проблемы в своих POINT_UPDATED действиях. Тем не менее, я добавил дополнительное действие, и это по-прежнему проблематично. Я добавил ссылку на игровую площадку TS в качестве правки к своему исходному сообщению. Если у вас будет возможность, вы можете взглянуть на это?

3. @Linda Paise По какой-то причине TS Playground отключила большую часть моего кода. Вместо этого я прикрепил ссылку CodeSandbox.

4. Кастинг as T кажется неправильным, но я полагаю, что это придется сделать. Я понимаю ваше объяснение из вашей правки. Спасибо.