Skip to content

TypeScript defintion of StoreEnhancer does not allow state extension with replaceReducer #3482

Closed
@mhelmer

Description

@mhelmer

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions