1
votes

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)

1

1 Answers

2
votes

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