I have here an annoying case where can't figure out why TS throws the bellow error:
src/store.ts:24:3 - error TS2322: Type 'Reducer<MemberState, InvalidateMembers>' is not assignable to type 'Reducer<MemberState, RootActions>'.
Types of parameters 'action' and 'action' are incompatible.
Type 'RootActions' is not assignable to type 'InvalidateMembers'.
Type 'InvalidateCatgories' is not assignable to type 'InvalidateMembers'.
24 member,
~~~~~~
src/store.ts:18:3
18 member: MemberState;
~~~~~~
The expected type comes from property 'member' which is declared here on type 'ReducersMapObject<RootState, RootActions>'
src/store.ts:25:3 - error TS2322: Type 'Reducer<CategoryState, InvalidateCatgories>' is not assignable to type 'Reducer<CategoryState, RootActions>'.
Types of parameters 'action' and 'action' are incompatible.
Type 'RootActions' is not assignable to type 'InvalidateCatgories'.
Type 'InvalidateMembers' is not assignable to type 'InvalidateCatgories'.
25 category,
~~~~~~~~
src/store.ts:19:3
19 category: CategoryState;
~~~~~~~~
The expected type comes from property 'category' which is declared here on type 'ReducersMapObject<RootState, RootActions>'
Why does it try to assign one interface to another (InvalidateMembers
to InvalidateCatgories
and vice versa)? The only way I can get rid of the error is by changing the type of 'type' to string (so the two interfaces have identical structure) in the interfaces like:
interface InvalidateMembers extends Action {
type: string;
}
Its puzzles me so much. I already triple checked everything + inscpected all redux types but can't understand the why the error.
-- UPDATE: --
After inspecting the redux types a little more, I realised that the ReducersMapObject
brings back each property of the rootReducer
along the the whole RootActions
object as one, which obviously won't match a single property any more. I think this is more of an issue of the design of the type itself, or?
export type Reducer<S = any, A extends Action = AnyAction> = (
state: S | undefined,
action: A
) => S
/**
* Object whose values correspond to different reducer functions.
*
* @template A The type of actions the reducers can potentially respond to.
*/
export type ReducersMapObject<S = any, A extends Action = Action> = {
[K in keyof S]: Reducer<S[K], A>
}
I would really appreciate your feedback.
store.js
...
export interface RootState {
member: MemberState;
category: CategoryState;
}
export type RootActions = MemberAction | CategoryAction;
const rootReducer = combineReducers<RootState, RootActions>({
member,
category,
});
export const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk as ThunkMiddleware<RootState, RootActions>))
);
actions/member.js
export enum MemberActionTypes {
INVALIDATE_MEMBERS = 'INVALIDATE_MEMBERS'
}
interface InvalidateMembers extends Action {
type: MemberActionTypes.INVALIDATE_MEMBERS;
}
export const invalidateMembers = (): ThunkResult<void> => (dispatch) => {
dispatch({
type: MemberActionTypes.INVALIDATE_MEMBERS
});
};
export type MemberAction = InvalidateMembers;
actions/category.js
export enum CategoryActionTypes {
INVALIDATE_CATEGORIES = 'INVALIDATE_CATEGORIES'
}
interface InvalidateCatgories extends Action {
type: CategoryActionTypes.INVALIDATE_CATEGORIES;
}
export const invalidateCategories = (): ThunkResult<void> => (dispatch) => {
dispatch({
type: CategoryActionTypes.INVALIDATE_CATEGORIES
});
};
export type CategoryAction = InvalidateCatgories;
reducers/member.js
export interface MemberState {
items: {};
}
const initialState = {
items: {}
};
export const member: Reducer<MemberState, MemberAction> = (state = initialState, action) => {
switch (action.type) {
case MemberActionTypes.INVALIDATE_MEMBERS:
return {
...state,
didInvalidate: true
};
default:
return state;
}
};
reducers/category.js
export interface CategoryState {
items: {};
}
const initialState = {
items: {},
};
export const category: Reducer<CategoryState, CategoryAction> = (state = initialState, action) => {
switch (action.type) {
case CategoryActionTypes.INVALIDATE_CATEGORIES:
return {
...state,
didInvalidate: true
};
default:
return state;
}
};
The behavior that you are seeing is by design. Think about what the root reducer created by combineReducers
actually does. It gets a state and and action. Then in order to update each piece of the state, it calls the reducer for that piece with the action. What that means is every reducer receives every action.
When the category reducer gets called with a member action, it simply does nothing (case default:
) and returns the existing category state unchanged. But your types must allow for the reducer category
to be called with an action of type MemberAction
because it will get called with those actions.
Update your typings so that each reducer can accept the union of all possible actions, which you have already defined as RootActions
.
export const member: Reducer<MemberState, RootActions>
export const category: Reducer<CategoryState, RootActions>
If for some reason you were insistent that each reducer should only be able to take it's own action type, that is possible but it's more work. What you would need to do it write your own combineReducers
which examines the action.type
to see if it is a MemberAction
or a CategoryAction
. Instead of passing the option off to all of the reducers, it would only call the reducer which matches the action and assume no changes to the rest.
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