Skip to content

combineReducers is broken with TypeScript 2.6.1 and Redux 3.7.2 #2709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
lenguyenthanh opened this issue Nov 12, 2017 · 28 comments
Closed

combineReducers is broken with TypeScript 2.6.1 and Redux 3.7.2 #2709

lenguyenthanh opened this issue Nov 12, 2017 · 28 comments

Comments

@lenguyenthanh
Copy link

lenguyenthanh commented Nov 12, 2017

combineReducers is broken with TypeScript 2.6.1 and Redux 3.7.2

What is the current behavior?
Here is my code

import State from './state'
import { defaultState } from './state'
import { combineReducers, Reducer } from 'redux'
import { Dispatch, Store, Action } from 'redux'
import { reducer as count } from '../feature/counter/counterScreen'

export enum keys {
  INCREMENT = 'increment',
  DECREMENT = 'decrement',
}

interface IncrementAction {
  readonly type: keys.INCREMENT
  readonly payload: {
    readonly size: number
  }
}
const incrementAction = (size: number): IncrementAction => ({
  type: keys.INCREMENT,
  payload: {
    size: size,
  },
})

interface DecrementAction {
  readonly type: keys.DECREMENT
  readonly payload: {
    readonly size: number
  }
}
const decrementAction = (size: number): DecrementAction => ({
  type: keys.DECREMENT,
  payload: {
    size: size,
  },
})

export type ActionTypes = IncrementAction | DecrementAction

/** REDUCER */
export function reducer(state = defaultState.count, action: ActionTypes) {
  switch (action.type) {
    case keys.DECREMENT:
      return state - action.payload.size
    case keys.INCREMENT:
      return state + action.payload.size
    default:
      return state
  }
}

const rootReducers: Reducer<State> = combineReducers({ count })

export default rootReducers

and here is error:

yarn build v0.27.5
$ yarn run clean && yarn run lint && yarn tsc --
src/app/reducers.ts(53,54): error TS2345: Argument of type '{ count: (state: number | undefined, action: ActionTypes) => number; }' is not assignable to parameter of type 'ReducersMapObject'.
  Property 'count' is incompatible with index signature.
    Type '(state: number | undefined, action: ActionTypes) => number' is not assignable to type 'Reducer<any>'.
      Types of parameters 'action' and 'action' are incompatible.
        Type 'AnyAction' is not assignable to type 'ActionTypes'.
          Type 'AnyAction' is not assignable to type 'DecrementAction'.
            Property 'payload' is missing in type 'AnyAction'.

What is the expected behavior?
It should compile and work.

Which versions of Redux, and which browser and OS are affected by this issue? Did this work in previous versions of Redux?
Redux 3.7.2 and Typescript 2.6.1

Hot fix

It works well if I change Reducer definition in index.d.ts file:

export type Reducer<S, A extends AnyAction> = (state: S, action: A) => S;

And my reducer:

const rootReducers: Reducer<State, ActionTypes> = combineReducers({ count })

How do you think about this issue and hot fix?

@timdorr
Copy link
Member

timdorr commented Nov 13, 2017

This is likely fixed on the next branch. Wait for a 4.0 prerelease or use the index.d.ts file from there now.

@timdorr timdorr closed this as completed Nov 13, 2017
@lenguyenthanh
Copy link
Author

Thank you so much for answer. Should we keep it open for other people see it? I couldn't search an issue for it before.

@mtgto
Copy link

mtgto commented Dec 7, 2017

Hi, I faced same trouble like @lenguyenthanh .
Is there any way to use HEAD definition instead of ./node_modules/redux/index.d.ts?

I try to put new definition file to "./typings/redux.d.ts", but it seems to be ignored while compiling.

Is there no way without overwrite ./node_modules/redux/index.d.ts?

@timdorr
Copy link
Member

timdorr commented Dec 7, 2017

These are on the next tag on npm right now with 4.0.0-beta.1. npm install redux@next

@mtgto
Copy link

mtgto commented Dec 8, 2017

@timdorr Thanks a lot!

@huestack
Copy link

@timdorr

I tried npm install redux@next, which duly added redux v4.0.0-beta.1, but still failed to compile. The error won't go away. Is there anything I can do?


$ tsc
src/App.tsx(8,33): error TS2345: Argument of type '{ seller: (state: SellerProps | undefined, action: SellerActionProps) => SellerProps; }' is not assignable to parameter of type 'ReducersMapObject<{ seller: SellerProps; }, Action<any>>'.
  Types of property 'seller' are incompatible.
    Type '(state: SellerProps | undefined, action: SellerActionProps) => SellerProps' is not assignable to type 'Reducer<SellerProps, Action<any>>'.
      Types of parameters 'action' and 'action' are incompatible.
        Type 'Action<any>' is not assignable to type 'SellerActionProps'.
          Property 'name' is missing in type 'Action<any>'.

App.tsx
reducer.tsx
package.json

@timdorr
Copy link
Member

timdorr commented Dec 11, 2017

You should be putting that name property into the payload of the action.

@huestack
Copy link

@timdorr I am actually using payload in action props. However, I changed my code to resemble to what @lenguyenthanh posted. Still no luck.

import { combineReducers } from 'redux';

export enum SellerActionKeys {
    LOAD_SELLER = 'LOAD_SELLER',
    ADD_SELLER = 'ADD_SELLER'
}

export interface SellerLoadAction {
    readonly type: SellerActionKeys.LOAD_SELLER;
    readonly payload: {
        readonly name: string;
    };
}

export interface SellerAddAction {
    readonly type: SellerActionKeys.ADD_SELLER;
    readonly payload: {
        readonly name: string;
    };
}

export type ActionTypes = SellerLoadAction | SellerAddAction;

export const seller = (state: any = { name: '' }, action: ActionTypes) => {
    switch (action.type) {
        case SellerActionKeys.LOAD_SELLER:
        case SellerActionKeys.ADD_SELLER:
            return {
                ...state,
                name: action.payload.name
            };
        default: return state;
    }
};

export const reducers = combineReducers({
    seller
});

Error:

$ tsc
src/App.tsx(7,34): error TS2345: Argument of type '{ seller: (state: any, action: ActionTypes) => any; }' is not assignable to parameter of type 'ReducersMapObject<{ seller: any; }, Action<any>>'.
  Types of property 'seller' are incompatible.
    Type '(state: any, action: ActionTypes) => any' is not assignable to type 'Reducer<any, Action<any>>'.
      Types of parameters 'action' and 'action' are incompatible.
        Type 'Action<any>' is not assignable to type 'ActionTypes'.
          Type 'Action<any>' is not assignable to type 'SellerAddAction'.
            Property 'payload' is missing in type 'Action<any>'.

I think, after introduction to strictFunctionTypes switch with Typescript 2.6.1, Action<any> and any extended interface will not match. I am not sure how @lenguyenthanh managed to compile his code with [email protected] and [email protected], but I could only compile by setting strictFunctionTypes: false in tsconfig.json.

@tipng
Copy link

tipng commented Dec 18, 2017

@timdorr I am also facing the same issue here.

as @huestack said the only way to go around this so far was to disable the strictFunctionTypes flag in the tsconfig.

@huestack
Copy link

@tipng
I am using this workaround without disabling strictFunctionTypes:

export type ActionTypes = SellerLoadAction | SellerAddAction;

const seller = (state: any = { name: '' }, action: ActionTypes) => {
    switch (action.type) {
        case SellerActionKeys.LOAD_SELLER:
        case SellerActionKeys.ADD_SELLER:
            return {
                ...state,
                name: action.payload.name
            };
        default: return state;
    }
};
export default (state: any, action: Action<string>) => seller(state, action as ActionTypes);

I hope it helps.

nloomans added a commit to metiscoderclass/rooster.hetmml.nl that referenced this issue Jan 6, 2018
Except for functions because of reduxjs/redux#2709
@seepel
Copy link

seepel commented Jan 12, 2018

Not very satisfying, but I've found that asserting the ReducersObjectMap to any gets me compiling again.

const combinedReducers = combineReducers({ reducer1, reducer2 } as any);

Using the version 4 beta typings does not work for me.

@timdorr
Copy link
Member

timdorr commented Jan 12, 2018

Check #2773. It needs a review.

@flexdeep
Copy link

Try to add "any" as an alternative data type in you reducer:

export function reducer(state = defaultState.count, action: ActionTypes | any) { ... }

@stiofand
Copy link

Using Typescript 3.2.4 and Redux 4.0.1, the issue remains. Used @seepel solution for now.

@samjmck
Copy link

samjmck commented Feb 1, 2019

Breaks for me as soon as I add a second parameter to the createStore method, i.e. the middleware.
Example:

import { createActionCreators, createReducerFunction, ImmerReducer } from 'immer-reducer';
import { applyMiddleware, combineReducers, createStore, Reducer, Store } from 'redux';
import thunk from 'redux-thunk';

interface IUser {
    id: number;
    name: string;
}

type UsersById = { [id: number]: IUser };

interface IGlobalState {
    usersById: UsersById;
}

type UserActions = {
    type: 'create',
    payload: {
        id: number;
        name: string;
    },
};

const userReducer: Reducer<UsersById, UserActions> = (state = {}, action) => {
    switch(action.type) {
        case 'create':
            const { id, name } = action.payload;
            return Object.assign(state, {
                [id]: {
                    id,
                    name,
                },
            });
    }
};

type Actions = UserActions;

function createGlobalStore(): Store<IGlobalState, Actions> {
    return createStore(
        combineReducers({ usersById: userReducer, }),
        applyMiddleware(thunk), // works without this line
    );
}

Full error: https://i.imgur.com/hH4TpyZ.png

@reduxjs reduxjs deleted a comment from wpadilla Mar 11, 2019
@reduxjs reduxjs deleted a comment from kishoremvrs Mar 11, 2019
@codeflowee
Copy link

The problem lies here https://github.com/reduxjs/redux/blob/master/index.d.ts#L62, if undefined is removed then it will be ok

@timdorr
Copy link
Member

timdorr commented Jul 8, 2019

@codeflowee The reducer will receive an undefined state on init, so that is a valid type.

@snapwich
Copy link

snapwich commented Jul 10, 2019

Ran into this same issue... Perhaps ReducersMapObject should not directly use the Reducer type then? While reducers may receive undefined state on init, you're not going to pass undefined state to combineReducers.

@nathguen
Copy link

nathguen commented Jul 16, 2019

My issue ended up being that I had forgotten to pass in an initial state (i.e., defaultState):

export interface NavigationState {
  drawerIsOpen: boolean;
}

export const defaultState: NavigationState = {
  drawerIsOpen: false
};

export default function reducer(state: NavigationState = defaultState, action: any): NavigationState {
  switch (action.type) {
    case NavigationReduxAction.SET_DRAWER_IS_OPEN:
      return {
        ...state,
        drawerIsOpen: action.drawerIsOpen
      };
  
    default:
      return state;
  }
}

@hamidmayeli
Copy link

I still have the issue using Typescript 3.2.6 and Redux 4.0.4. So forced to cast to any.

@RobinLebhar
Copy link

RobinLebhar commented Oct 3, 2019

Same problem : Typescript : 3.6.3 and Redux 4.0.4

Force to cast as any, an so I don't have any autocomplete on my mapStateToProps' state :(

If you want an alternative to have the autocomplete in your mapStateToProps

const mapStateToProps = (state: any): StateProps => {
  return {
    quizData: (state as IStore).quizz.results
  }
}

The IStore is just an Interface made of all my Reducers's InitialState

export interface IStore {
    quizz: IQuizInitialState
}

@stepankuzmin
Copy link

Same here: Typescript 3.6.4, Redux 4.0.4

@industry7
Copy link

Can this be reopened? It's still a problem. Redux 4.0.4, TypeScript 3.7.2

@the-unbearable-lightness-of-being
Copy link

the-unbearable-lightness-of-being commented Dec 21, 2019

For me, I was facing issues that required casting to any, but it turns out I just didn't have redux installed, in this case, version 4.0.4. Somehow, even without redux installed, I was able to import createStore, applyMiddleware, and combineReducers from a package called redux. No clue what was going on there. In the end, my working, strongly typed setup looks this:

// store/index.ts ------------------------------
import { createStore, applyMiddleware, combineReducers } from "redux"
import { composeWithDevTools } from "redux-devtools-extension"
import logger from "redux-logger"
import { appReducer as app } from "@services/app/reducer"
import { searchReducer as search } from "@services/search/reducer"

// create the root reducer
export const rootReducer = combineReducers({ app, search })

// create the store
export const store = createStore(
  rootReducer,
  // initialize redux devtools and the redux logger
  composeWithDevTools(applyMiddleware(logger)),
)

// types/redux.d.ts ------------------------------
import { rootReducer } from "@store"
import { setEnvironment, setMode, setTheme } from "@services/app/action"
import { setSearchBy } from "@services/search/action"

/**
 * `declare global` is used inside a file that has `import`
 * or `export` to declare things in the global scope.
 * This is necessary in files that contain `import` or
 * `export` since such files are considered modules,
 * and anything declared in a module is in the module scope.
 *
 * Using `declare global` in a file that is not a module
 * (that is contains no `import`/`export`) is an error since
 * everything in such a file is in the global scope anyway.
 *
 */
declare global {
  /**
   * This is the single source of truth for all components
   * that access the redux store. It infers the apps store
   * using the return type of the root reducer.
   */
  // type Store = ReduxState
  type Store = ReturnType<typeof rootReducer>

 // define the app state for the app reducer
  type AppState = { theme: Theme; mode: Mode; environment: Environment }

  // define the search state for the search reducer
  type SearchState = { searchBy: SearchType }

  // bundle app actions for the app reducer action parameter
  type AppActionTypes =
    | ReturnType<typeof setTheme>
    | ReturnType<typeof setMode>
    | ReturnType<typeof setEnvironment>

  // bundle search actions for the search reducer action parameter
  type SearchActionTypes = ReturnType<typeof setSearchBy>

  // combine all possible action types
  type AppActions = AppActionTypes | SearchActionTypes
}

// app/reducer.ts  ------------------------------
import constants from "../constants"

const initialState: AppState = {
  theme: "light",
  mode: "search",
  environment: "lower",
}

export const appReducer = (
  state = initialState,
  action: AppActionTypes,
): AppState => {
  switch (action.type) {
    case constants.THEME:
      return { ...state, theme: action.payload }

    case constants.MODE:
      return { ...state, mode: action.payload }

    case constants.ENVIRONMENT:
      return { ...state, environment: action.payload }

    default:
      return state
  }
}

// app/action.ts  ------------------------------
import constants from "@constants"

/**
 * Set the theme of the app.
 * @param {Theme} theme
 */
export const setTheme = (theme: Theme) =>
  ({
    type: constants.THEME,
    payload: theme,
  } as const)

/**
 * Set the mode of the app.
 * @param {Mode} mode
 */
export const setMode = (mode: Mode) =>
  ({
    type: constants.MODE,
    payload: mode,
  } as const)

/**
 * Set the environment of the app.
 * @param {Environment} environment
 */
export const setEnvironment = (environment: Environment) =>
  ({
    type: constants.ENVIRONMENT,
    payload: environment,
  } as const)

gilbsgilbs added a commit to gilbsgilbs/redux that referenced this issue Jan 14, 2020
@tylim88
Copy link

tylim88 commented May 30, 2020

redux 4.0.5
typescript 3.8.3

issue persists

edit: I found a solution, simply pass you store state type to combineReducers and the error goes away
1

@theanhmx
Copy link

theanhmx commented Aug 11, 2020

Not very satisfying, but I've found that asserting the ReducersObjectMap to any gets me compiling again.

const combinedReducers = combineReducers({ reducer1, reducer2 } as any);

Using the version 4 beta typings does not work for me.

it will break the typing, if you need to types store base on the rootReducer as I do here

export type RootState = ReturnType<typeof rootReducer>;

instead,
this worked for me const persistor = persistStore(store as any);

@bledar
Copy link

bledar commented Jun 10, 2021

Creating a function inside configureStore to return all reducers will help to use it as a root reducer. Also, typescript will not throw any error. Typing will work as usual.
I faced this problem and I solved. Hopefully, this will help someone :)

const allReducers = {
  auth: authReducer,
  projects: projectsReducer,
  navigation: navigationReducer
};

export const appReducer = combineReducers<typeof allReducers>(allReducers);

export type AppState = ReturnType<typeof appReducer>

export const store = configureStore({
  reducer: (state, action) => {
    if (action.type === 'auth/logout') {
      return appReducer(undefined, action);
    }
    return appReducer(state, action);
  },
  devTools: process.env.NODE_ENV !== 'production',
});

export type AppDispatch = typeof store.dispatch;
export type RootState = AppState;

export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

@sajumani
Copy link

I modified Little & works good!

export function combineReducers(reducers: any) {
type keys = keyof typeof reducers;
type returnType = { [K in keys]: ReturnType<typeof reducers[K]> }

return (state:any, action:any) => {

const newState: returnType = {} as any;
const keys = Object.keys(reducers);
keys.forEach(key => {
  const result = reducers[key](state[key], action);
  newState[key] = result || state[key];
});
return newState;

}
}

@reduxjs reduxjs locked as resolved and limited conversation to collaborators Jul 16, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests