Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Redux + Typescript: Types of parameters 'action' and 'action' are incompatible

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;
  }
};
like image 753
Edmond Tamas Avatar asked Dec 20 '18 09:12

Edmond Tamas


1 Answers

Problem

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.

Solution

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>

Alternative

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.

like image 121
Linda Paiste Avatar answered Oct 22 '22 19:10

Linda Paiste