TypeScript defintion of StoreEnhancer does not allow state extension with replaceReducer
See original GitHub issueDo 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:
- Created 4 years ago
- Comments:30 (20 by maintainers)
Top GitHub Comments
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 likeOK!! 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:
Hopefully this is the right solution! Let me know if it works for you as well as it does here @mhelmer