Asaf Aviv
Asaf Aviv

Reputation: 11780

Typing higher order reducer; Types of parameters 'state' and 'state' are incompatible

I'm trying to type an higher order reducer and managed to narrow down the problem, the problem is from state type in redux type definition of Reducer which is S | undefined, If i delete undefined and just type state as S everything works as expected

export type Reducer<S = any, A extends Action = AnyAction> = (
  state: S | undefined,
  action: A
) => S

The problem is that the higher order reducer doesn't initialize the state with a default value which is required by redux as it dispatches an action that doesn't match in any reducer to populate the store with an initial value but i guess typescript cant tell that the inner reducer will handle that.

What are my options here besides removing undefined from the type definition?

The type of movie in src > reducers.ts > RootState at the bottom is never

Sandbox

Higher order reducer

import { Action } from "redux";

const startReducer = <S>(state: S): S => ({
  ...state,
  loading: true,
  error: false
});

const successReducer = <S>(state: S): S => ({
  ...state,
  loading: false
});

const errorReducer = <S>(state: S): S => ({
  ...state,
  error: true,
  loading: false
});

type LoadingActionTypes = Record<"start" | "success" | "error", string>;

const withLoadingStates = ({ start, success, error }: LoadingActionTypes) => {
  const actionReducerMapper = {
    [start]: startReducer,
    [success]: successReducer,
    [error]: errorReducer
  };

  return <S, A extends Action>(baseReducer: (state: S, action: A) => S) => (
    state: S,
    action: A
  ): S => {
    const nextState = actionReducerMapper[action.type]
      ? actionReducerMapper[action.type](state)
      : state;

    return baseReducer(nextState, action);
  };
};

export default withLoadingStates;

Inner reducer

import { combineReducers } from "redux";
import withLoadingStates from "./withLoadingStates";

const FETCH_MOVIE_BY_ID_START = "FETCH_MOVIE_BY_ID_START";
const FETCH_MOVIE_BY_ID_SUCCESS = "FETCH_MOVIE_BY_ID_SUCCESS";
const FETCH_MOVIE_BY_ID_ERROR = "FETCH_MOVIE_BY_ID_ERROR";

type FetchMovieByIdStartAction = { type: typeof FETCH_MOVIE_BY_ID_START };
type FetchMovieByIdSuccessAction = { type: typeof FETCH_MOVIE_BY_ID_SUCCESS };
type FetchMovieByIdErrorAction = { type: typeof FETCH_MOVIE_BY_ID_ERROR };

type MovieByIdActionTypes =
  | FetchMovieByIdStartAction
  | FetchMovieByIdSuccessAction
  | FetchMovieByIdErrorAction;

type LoadingStates = {
  loading: boolean;
  error: boolean;
};

const initialState: LoadingStates = {
  loading: false,
  error: false
};

const movieReducer = (state = initialState, action: MovieByIdActionTypes) =>
  state;

const wrappedReducer = withLoadingStates({
  start: FETCH_MOVIE_BY_ID_START,
  success: FETCH_MOVIE_BY_ID_SUCCESS,
  error: FETCH_MOVIE_BY_ID_ERROR
})(movieReducer);

export const rootReducer = combineReducers({
  movie: wrappedReducer
});

// movie type is never
export type RootState = ReturnType<typeof rootReducer>;

The error i am getting

(property) movie: Reducer No overload matches this call. Overload 1 of 2, '(reducers: ReducersMapObject<{ movie: LoadingStates; trending: Record; }, any>): Reducer<...>', gave the following error. Type '(state: LoadingStates, action: MovieByIdActionTypes) => LoadingStates' is not assignable to type 'Reducer'. Types of parameters 'state' and 'state' are incompatible. Type 'LoadingStates | undefined' is not assignable to type 'LoadingStates'. Type 'undefined' is not assignable to type 'LoadingStates'.ts(2769)

Upvotes: 1

Views: 1098

Answers (1)

Harald Gliebe
Harald Gliebe

Reputation: 7564

You can modify your withLoadingState higher order reducer as follows:

const withLoadingStates = ({ start, success, error }: LoadingActionTypes) => {
  const actionReducerMapper = {
    [start]: startReducer,
    [success]: successReducer,
    [error]: errorReducer
  };

  return <S, A extends Action>(
    baseReducer: (state: S | undefined, action: A) => S
  ) => (state: S | undefined, action: A): S => {
    const nextState = actionReducerMapper[action.type]
      ? actionReducerMapper[action.type](state)
      : state;

    return baseReducer(nextState, action);
  };
};

Then the types enforce that it will return an wrapped reducer that accepts undefined for initialisation and also requires that the baseReducer returns a State when it is invoked with an undefined value. Here is the modified sandbox: https://codesandbox.io/s/bold-visvesvaraya-15f5o

Upvotes: 2

Related Questions