Problem
I have two reducers that use the exact same logic for two different arrays containing points. An array will only have points of a single type; these types are A
and B
. I want to create a reducer that accepts an array of points of type A
or type B
and then returns an array of that same type.
When I try to compile the code below, I get this error:
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>'.
How can I fix this?
Problematic Code
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
For reference, here is the updateObject
function:
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 Function
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;
};
EDIT #1
Here is a CodeSandbox link to my latest attempt based on Linda Paiste's answer. At this time there is still an issue assigning to type Partial<T>
.
CodePudding user response:
I started trying to fill in the missing pieces of your code in order to find where the problem is. Doing that, it became apparent that the issue is in your Action
type (as suggested by @Alex Chashin).
I know that Actions
has a type
which needs to include POINT_UPDATED
. It also has a payload
. The payload includes an id
, a category
, and some newValues
.
type Actions = {
type: typeof POINT_UPDATED;
payload: {
id: string;
category: Category;
newValues: ????;
}
}
What is newValues
? Based on the signature of updateObject
, we know that it should be Partial<T>
. But Actions
doesn't know what T
is.
I'm guessing that your code uses something like newValues: A | B;
, which gives me the error that you posted. This is not specific enough. It's not enough to know that our new values are "A
or B
". updateObject
says that if T
is A
then newValues
must be A
and if T
is B
then newValues must be B
.
Therefore your Actions
needs to be a generic type.
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[]
...
I'm seeing an error on your updateItemInArray
when trying to access item.id
:
Property 'id' does not exist on type 'T'
You need to refine the type of T
such it knows about the id property:
const updateItemInArray = <T extends {id: string}>(
Doing this causes new errors in your genericReducer
because you have said that T
must have a category
but you haven't said that it must have an id
. We can fix that with:
const genericReducer = <T extends (A | B) & {id: string}>(
which is the same as
const genericReducer = <T extends {id: string; category: Category}>(
Though it actually appears that you are never looking at the category
on your points T[]
. So all you really need is:
const genericReducer = <T extends {id: string}>(
Edit:
The error that you are getting in your revised code is honestly really dumb. Technically T
extends
a type with {elevation: number}
. So there is the potential that it could require a more specific version of the type, like {elevation: 99}
. In that particular case, {elevation: number}
would not be assignable to Partial<{elevation: 99}>
.
At least I thought that was the problem. However my first fix did not work. I tried to refine the type of Actions
to say that the elevation
property must match the one from T
.
type Payload<T extends A_or_B> = {
[ActionTypes.FETCH_SUCCEEDED]: {
id: string;
elevation: T['elevation'];
timestamp: number;
};
...
But I'm still getting an error, so now I'm thoroughly confused.
Argument of type
{ elevation: T["elevation"]; }
is not assignable to parameter of typePartial<T>
I really can't explain that one.
You might have to make an assertion instead. (Don't bother with the fix above).
return updateObject<{elevation: number}>(stpt, {
elevation: action.payload.elevation,
}) as T;
We avoid errors within the updateObject
function by setting the generic T
of that function to {elevation: number}
. Both stpt
(type T
) and {elevation: number}
are assignable to that type.
But now the return type of the updateObject
function is {elevation: number}
instead of T
. So we need to assert as T
in order to change the type.