Can some TypeScript master help me figure out why this code to create store enhancers in redux 4.0 is giving me type errors?
The minimal repo is here. It's creating a do-nothing store ehnahcer. After cloning, install dependencies by running yarn
. To see the error, run yarn run compile
.
The relevant file is
import {
StoreEnhancer,
StoreEnhancerStoreCreator,
Reducer,
DeepPartial,
AnyAction,
} from 'redux';
interface RootState {
someKey: string;
}
export const enhancer: StoreEnhancer =
(createStore: StoreEnhancerStoreCreator): StoreEnhancerStoreCreator =>
(reducer: Reducer<RootState, AnyAction>, preloadedState?: DeepPartial<RootState>) => {
const store = createStore(reducer, preloadedState);
const newDispatch: typeof store.dispatch = <A extends AnyAction>(action: A) => {
const result = store.dispatch(action);
return result;
}
return {
...store,
dispatch: newDispatch,
};
};
The error I'm getting is
src/enhancer.ts(15,5): error TS2322: Type '(reducer: Reducer<RootState, AnyAction>, preloadedState?: DeepPartial<RootState> | undefined) => ...' is not assignable to type 'StoreEnhancerStoreCreator<{}, {}>'.
Types of parameters 'reducer' and 'reducer' are incompatible.
Types of parameters 'state' and 'state' are incompatible.
Type 'RootState | undefined' is not assignable to type 'S | undefined'.
Type 'RootState' is not assignable to type 'S | undefined'.
Type 'RootState' is not assignable to type 'S'.
I don't understand why RootState
is not assignable to S
.
We strongly recommend using TypeScript in Redux applications. However, like all tools, TypeScript has tradeoffs. It adds complexity in terms of writing additional code, understanding TS syntax, and building the application.
And an enhancer is basically if we wanted to add additional functionality to a Redux store. This is a place where we can pass in a function which will get an instance of createStore, allow us to do whatever we want and then pass all the arguments into it.
Reducers are a type of function that can perform these task for state management. Because Reducers are built into the React API, you don't necessarily have to use a separate state-management library like Redux or MobX.
Lets deep dive at how StoreEnhancerStoreCreator
is defined
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = {}> = <S = any, A extends Action = AnyAction>(reducer: Reducer<S, A>, preloadedState?: DeepPartial<S>) => Store<S & StateExt, A> & Ext;
This is generic type with two type arguments, Ext
and StateExt
. The result of this type is another generic function. For example if we do
const genericStoreCreator: StoreEnhancerStoreCreator = <S = any, A extends Action = AnyAction>(r: Reducer<S, A>) => createStore(r)
genericStoreCreator
will still be generic function. We already used generic type StoreEnhancerStoreCreator
without providing type arguments to it (with default type values), but result is still generic function.
Then we may call genericStoreCreator
providing specific type arguments for S
and A
.
const store = genericStoreCreator((state: RootState = { someKey: 'someKey' }, action: AnyAction) => state)
And now we have specific store
without any generics.
You may consider StoreEnhancerStoreCreator
as generic inside generic.
Now lets get back to store enhancers. From source (didn't find it in documentation)
A store enhancer is a higher-order function that composes a store creator to return a new, enhanced store creator.
So result of store enhancer should still be generic store creator. Inside enhancer function we shouldn't relate on specific shape of state (i.e. it have someKey
property) or specific shape of action (except it has type
property). When implementing enhancer we don't know what exact store it will enhance.
To make your code working, just carry generics inside enhancer code
export const enhancer: StoreEnhancer =
(createStore: StoreEnhancerStoreCreator): StoreEnhancerStoreCreator =>
<S = any, A extends Action = AnyAction>(reducer: Reducer<S, A>, preloadedState?: DeepPartial<RootState>) => {
const store = createStore(reducer, preloadedState);
const newDispatch: Dispatch<A> = (action) => {
const result = store.dispatch(action);
return result;
}
return {
...store,
dispatch: newDispatch,
};
};
Take a note that enhanced store creator is still generic and can be user with any state and actions.
But what can we enhance with enhancer? As source says
@template Ext Store extension that is mixed into the Store type.
@template StateExt State extension that is mixed into the state type.
Lets extend store with print5
function which when called will log 5 to console.
export const enhancer: StoreEnhancer<{ print5: () => void }> =
(createStore: StoreEnhancerStoreCreator): StoreEnhancerStoreCreator<{ print5: () => void }> =>
<S = any, A extends Action = AnyAction>(reducer: Reducer<S, A>, preloadedState?: DeepPartial<RootState>) => {
const store = createStore(reducer, preloadedState);
const newDispatch: Dispatch<A> = (action) => {
const result = store.dispatch(action);
return result;
}
return {
...store,
dispatch: newDispatch,
print5 () { console.log(5) },
};
};
Again take a note that resulting store is still generic.
And here is simple demo. Take a note how print5
function is propagated from enhancer to resulting store.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With