I have a reducer that has an action creator that can be an array of two different types of object, each has their own interface. However, I am getting this error
Type '(A | B)[]' is not assignable to type 'B[]'.
Type 'A | B' is not assignable to type 'B'.
Property 'productionId' is missing in type 'A' but required in type 'B'
I suspect this error is due to both interface having similar values, except B has one extra value than A?
Here is the typescript playground
Here is the full reproducible code
interface A {
id: number;
name: string;
}
interface B {
id: number;
productionId: number;
name: string;
}
interface IAction<Data> {
type: string;
data: Data;
}
interface ISelectionOptionsReducerState {
a: A[];
b: B[];
}
let initialState: ISelectionOptionsReducerState = {
a: [],
b: []
};
type TAction = IAction<Array<A | B>>;
type TAction = IAction<A[] | B[]>; <= this didn't work either
type TReducer = (
state: ISelectionOptionsReducerState,
action: TAction
) => ISelectionOptionsReducerState;
const selectionOptionsReducer: TReducer = (
state: ISelectionOptionsReducerState = initialState,
action: TAction
): ISelectionOptionsReducerState => {
Object.freeze(state);
let newState: ISelectionOptionsReducerState = state;
switch (action.type) {
case '1':
newState.a = action.data;
break;
case '2':
newState.b = action.data; <= error happen here
break;
default:
return state;
}
return newState;
};
Couple things:
First,
Array<A | B>
(A | B)[]
are all identical.
Second, the reason why A
is assignable to both is because all properties of A are also in B.
Third, don't mutate state. Reassigning it isn't enough.
> const x = {}
undefined
> const y = x
undefined
> y.a = 1
1
> x
{ a: 1 }
You can splat into a new object: let newState = { ...state }
- that's generally sufficient.
Ok. You can't assign a value of type A | B
to something that's type B
. You've used something else (type
) to signal a different value, but TS can't know about that unless you tell it. There are a number of different ways you can do it.
First, assert:
newState.b = action.data as B[];
This is effectively telling TS to bugger off. More often than not, this is fine... If you're doing something really questionable, TS will make you assert to unknown
first. That's not the case, here, though.
but there are better ways to do it.
Slightly better: type guards
This requires refactoring out the switch:
function isA(x: any): x is IAction<Array<A>> {
return x.type === '1'
}
function isB(x: any): x is IAction<Array<B>> {
return x.type === '2'
}
...
if (isA(action)) {
newState.a = action.data;
} else if (isB(action)) {
newState.b = action.data;
}
(note: I can't actually get this to work... the code is right, I'm just getting type never
for action after the first check - not sure what's going on here)
Finally, let TypeScript do the resolution for you via duck-typing.
The tl;dr of that is that if you have a property within an object that correlates with a type, TS can automatically pick up types if that property is sufficiently unique.
I suspect this error is due to both interface having similar values, except B has one extra value than A?
Yes, you can assign B to A, but not A to B.
You need type guard:
function isA(data: A | B): data is A {
return typeof (data as B).productionId === 'undefined'
}
function isB(data: A | B): data is B {
return typeof (data as B).productionId === 'string'
}
...
case '1':
newState.a = action.data.filter(isA);
break;
case '2':
newState.b = action.data.filter(isB);
break;
Edit: (I can't write comments)
@Tyler Sebastian
Array<A | B>
(A | B)[]
are identical, but A[] | B[]
is diffrent
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With