question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

TypeScript defintion of StoreEnhancer does not allow state extension with replaceReducer

See original GitHub issue

Do you want to request a feature or report a bug?

The StoreEnhancer TypeScript interface does not seem to have the appropriate type for the replaceReducer property of the enhanced store. I would consider it a bug in the type definition.

What is the current behavior?

When implementing the StoreEnhancer interface with ExtraState, the type signature of replaceReducer on the “enhanced” store is coupled to the type of the wrapped reducer.

Given a StoreEnhancer of type

StoreEnhancer<Ext, ExtraState>

the returned store is of type

Store<S & StateExt, A> & Ext

with the replaceReducer property as such

(nextReducer: Reducer<S & ExtraState, A>) => void

Returning a store with a replaceReducer that accepts the original reducer gives the following type-error:

Type '<S, A extends Action<any> = AnyAction>(reducer: Reducer<S, A>, preloadedState?: DeepPartial<S> | undefined) => { replaceReducer: (nextReducer: Reducer<S, A>) => void; dispatch: Dispatch<A>; getState(): S & ExtraState; subscribe(listener: () => void): Unsubscribe; [Symbol.observable](): Observable<...>; }' is not assignable to type 'StoreEnhancerStoreCreator<{}, ExtraState>'.
      Type '{ replaceReducer: (nextReducer: Reducer<S, A>) => void; dispatch: Dispatch<A>; getState(): S & ExtraState; subscribe(listener: () => void): Unsubscribe; [Symbol.observable](): Observable<S & ExtraState>; }' is not assignable to type 'Store<S & ExtraState, A>'.
        Types of property 'replaceReducer' are incompatible.
          Type '(nextReducer: Reducer<S, A>) => void' is not assignable to type '(nextReducer: Reducer<S & ExtraState, A>) => void'.
            Types of parameters 'nextReducer' and 'nextReducer' are incompatible.
              Types of parameters 'state' and 'state' are incompatible.
                Type 'S | undefined' is not assignable to type '(S & ExtraState) | undefined'.
                  Type 'S' is not assignable to type '(S & ExtraState) | undefined'.
                    Type 'S' is not assignable to type 'S & ExtraState'.
                      Type 'S' is not assignable to type 'ExtraState'.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar.

The type check fails for the following code:

import {
  StoreEnhancer,
  Action,
  AnyAction,
  Reducer,
  createStore,
  DeepPartial
} from "redux";

interface State {
  someField: "string";
}

interface ExtraState {
  extraField: "extra";
}

const reducer: Reducer<State> = null as any;

function stateExtensionExpectedToWork() {
  interface ExtraState {
    extraField: "extra";
  }

  const enhancer: StoreEnhancer<{}, ExtraState> = createStore => <
    S,
    A extends Action = AnyAction
  >(
    reducer: Reducer<S, A>,
    preloadedState?: DeepPartial<S>
  ) => {
    const wrappedReducer: Reducer<S & ExtraState, A> = null as any;
    const wrappedPreloadedState: S & ExtraState = null as any;
    const store = createStore(wrappedReducer, wrappedPreloadedState);
    return {
      ...store,
      replaceReducer: (nextReducer: Reducer<S, A>): void => {
        const nextWrappedReducer: Reducer<S & ExtraState, A> = null as any;
        store.replaceReducer(nextWrappedReducer);
      }
    };
  };

  const store = createStore(reducer, enhancer);
  store.replaceReducer(reducer);
}

See src/index.ts in the linked codesandbox example that implements the same function that would be expected to type-check, followed by another function of how it actually has to be done.

https://codesandbox.io/s/redux-store-enhancer-types-s6d3v

The example is based on the typescript test for the enhancer at test/typescript/enhancers.ts in this repo. The code doesn’t execute (due to unsafe casts of null as any), but it is the type check that is of interest here.

What is the expected behavior?

When a store is created with a store enhancer that wraps a reducer and adds state, I would expect that replaceReducer on the returned store can be called with the original rootReducer.

const store = createStore(rootReducer, preloadedState, enhancer)
// ...
store.replaceReducer(rootReducer)

It would be the responsibility of the enhancer to appropriately replace the wrapped reducer. I.e return a store such as:

{
  ...store,
  replaceReducer: (nextReducer: Reducer<S, A>) => {
    store.replaceReducer(wrapReducer(nextReducer))
  },
}

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

Redux version: 4.0.4, OS: Ubuntu 19.04 Browser: N/A Did this work in previous versions of Redux?: Not that I know of

Issue Analytics

  • State:open
  • Created 4 years ago
  • Comments:30 (20 by maintainers)

github_iconTop GitHub Comments

1reaction
mhelmercommented, Aug 30, 2019

Nice. I will try it out this evening.

Just one thing about that final example. The persistReducer function doesn’t add any extra state so it doesn’t really capture the issue. The type signature would rather be something like

function persistReducer<S, A>(reducer: Reducer<S, A>): Reducer<S & ExtraState, A> {
  // ...
}
1reaction
cellogcommented, Aug 29, 2019

OK!! I am satisfied I have found a solution that actually works.

First of all, the fix.

https://github.com/reduxjs/redux/pull/3524/commits/8ca829033dde4d4c45672865347840fe6985683c#diff-b52768974e6bc0faccb7d4b75b162c99L345

It turns out that by providing default values, type inference was short-circuiting, and causing the error.

Next, the tests were all using contrived non-realistic examples, so I fixed the tests, first here:

https://github.com/reduxjs/redux/pull/3524/commits/8ca829033dde4d4c45672865347840fe6985683c#diff-09c07e35fbab9793e5f037dc3c72c878L128

and then in a follow-up commit for the rest of the tests:

https://github.com/reduxjs/redux/pull/3524/commits/7e7395f733265d689ef91b56a8e208fd854c2aa3

As you can see, the final example of your code, now working, is:


function finalHelmersonExample() {
  function persistReducer<S>(config: any, reducer: S): S {
    return reducer
  }

  function persistStore<S>(store: S) {
    return store
  }

  function createPersistEnhancer(persistConfig: any): StoreEnhancer {
    return createStore => <S, A extends Action = AnyAction>(
      reducer: Reducer<S, A>,
      preloadedState?: any
    ) => {
      const persistedReducer = persistReducer(persistConfig, reducer)
      const store = createStore(persistedReducer, preloadedState)
      const persistor = persistStore(store)

      return {
        ...store,
        replaceReducer: nextReducer => {
          return store.replaceReducer(
            persistReducer(persistConfig, nextReducer)
          )
        },
        persistor
      }
    }
  }
}

Hopefully this is the right solution! Let me know if it works for you as well as it does here @mhelmer

Read more comments on GitHub >

github_iconTop Results From Across the Web

TypeScript definition for StoreEnhancer prevents rest ...
The problem is that the return type of composedEnhancers is {} , while createStore expects it to be StoreEnhancerStoreCreator<{}, {}> .
Read more >
Understanding TypeScript Type Inference | by Gregory Beaver
First, the Store that is returned does not know anything about the extension to the state returned from getState() or the extension to...
Read more >
how to time travel debugging at redux-devtools | hmos.dev
We will check how to time travel debugging at redux-devtools.
Read more >
Redux with Code-Splitting and Type Checking - Dropbox
Redux has become the go-to state management system for React applications. ... How do you code-split your store so you're not serving ...
Read more >
redux-toolkit.umd.min.js.map - UNPKG
unfinalizedDrafts_ < 1) {\n\t\t\t// optimization: if an object is not a draft, ... If you want to restrict the extension, specify the features...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found