Skip to content
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

fix replaceReducer with a store enhancer #3524

Merged
merged 21 commits into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,33 @@ export type Observer<T> = {
next?(value: T): void
}

/**
* Extend the state
*
* This is used by store enhancers and store creators to extend state.
* If there is no state extension, it just returns the state, as is, otherwise
* it returns the state joined with its extension.
*/
export type ExtendState<State, Extension> = [Extension] extends [never]
? State
: State & Extension

/**
* A store is an object that holds the application's state tree.
* There should only be a single store in a Redux app, as the composition
* happens on the reducer level.
*
* @template S The type of state held by this store.
* @template A the type of actions which may be dispatched by this store.
* @template StateExt any extension to state from store enhancers
* @template Ext any extensions to the store from store enhancers
*/
export interface Store<S = any, A extends Action = AnyAction> {
export interface Store<
S = any,
A extends Action = AnyAction,
StateExt = never,
Ext = {}
Comment on lines +274 to +275
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, would it be possible to make StateExt and Ext optional whenever not using an Enhancer?
At the moment createStore<S,A,StateExt,Ext> generic typings are required as opposed as createStore<S,A> for stores without enhancers. Thanks!

> {
/**
* Dispatches an action. It is the only way to trigger a state change.
*
Expand Down Expand Up @@ -326,9 +344,9 @@ export interface Store<S = any, A extends Action = AnyAction> {
*
* @param nextReducer The reducer for the store to use instead.
*/
replaceReducer<NewState = S, NewActions extends A = A>(
replaceReducer<NewState, NewActions extends Action>(
nextReducer: Reducer<NewState, NewActions>
): Store<NewState, NewActions>
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext

/**
* Interoperability point for observable/reactive libraries.
Expand All @@ -355,15 +373,15 @@ export type DeepPartial<T> = {
* @template StateExt State extension that is mixed into the state type.
*/
export interface StoreCreator {
<S, A extends Action, Ext, StateExt>(
<S, A extends Action, Ext = {}, StateExt = never>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S & StateExt, A> & Ext
<S, A extends Action, Ext, StateExt>(
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
<S, A extends Action, Ext = {}, StateExt = never>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext>
): Store<S & StateExt, A> & Ext
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
}

/**
Expand Down Expand Up @@ -417,16 +435,17 @@ export const createStore: StoreCreator
* @template Ext Store extension that is mixed into the Store type.
* @template StateExt State extension that is mixed into the state type.
*/
export type StoreEnhancer<Ext = {}, StateExt = {}> = (
next: StoreEnhancerStoreCreator
export type StoreEnhancer<Ext = {}, StateExt = never> = (
next: StoreEnhancerStoreCreator<Ext, StateExt>
) => StoreEnhancerStoreCreator<Ext, StateExt>
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = {}> = <

export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <
S = any,
A extends Action = AnyAction
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
) => Store<S & StateExt, A> & Ext
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

/* middleware */

Expand Down
2 changes: 1 addition & 1 deletion src/applyMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function applyMiddleware<Ext, S = any>(
): StoreEnhancer<{ dispatch: Ext }>
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer {
): StoreEnhancer<any> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fix ends up being fine because the overloads provide more restrictive typing (you can check in tests/typescript/middleware.ts to see that the stores returned are strictly typed)

return (createStore: StoreCreator) => <S, A extends AnyAction>(
reducer: Reducer<S, A>,
...args: any[]
Expand Down
50 changes: 36 additions & 14 deletions src/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
PreloadedState,
StoreEnhancer,
Dispatch,
Observer
Observer,
ExtendState
} from './types/store'
import { Action } from './types/actions'
import { Reducer } from './types/reducers'
Expand Down Expand Up @@ -37,20 +38,35 @@ import isPlainObject from './utils/isPlainObject'
* @returns {Store} A Redux store that lets you read the state, dispatch actions
* and subscribe to changes.
*/
export default function createStore<S, A extends Action, Ext, StateExt>(
export default function createStore<
S,
A extends Action,
Ext = {},
StateExt = never
>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S & StateExt, A> & Ext
export default function createStore<S, A extends Action, Ext, StateExt>(
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
export default function createStore<
S,
A extends Action,
Ext = {},
StateExt = never
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext>
): Store<S & StateExt, A> & Ext
export default function createStore<S, A extends Action, Ext, StateExt>(
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
export default function createStore<
S,
A extends Action,
Ext = {},
StateExt = never
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
enhancer?: StoreEnhancer<Ext>
): Store<S & StateExt, A> & Ext {
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
Expand All @@ -74,7 +90,7 @@ export default function createStore<S, A extends Action, Ext, StateExt>(

return enhancer(createStore)(reducer, preloadedState as PreloadedState<
S
>) as Store<S & StateExt, A> & Ext
>) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
}

if (typeof reducer !== 'function') {
Expand Down Expand Up @@ -252,7 +268,7 @@ export default function createStore<S, A extends Action, Ext, StateExt>(
*/
function replaceReducer<NewState, NewActions extends A>(
nextReducer: Reducer<NewState, NewActions>
): Store<NewState, NewActions> {
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
Expand All @@ -269,7 +285,13 @@ export default function createStore<S, A extends Action, Ext, StateExt>(
// the new state tree with any relevant data from the old one.
dispatch({ type: ActionTypes.REPLACE } as A)
// change the type of the store by casting it to the new store
return (store as unknown) as Store<NewState, NewActions>
return (store as unknown) as Store<
ExtendState<NewState, StateExt>,
NewActions,
StateExt,
Ext
> &
Ext
}

/**
Expand Down Expand Up @@ -317,12 +339,12 @@ export default function createStore<S, A extends Action, Ext, StateExt>(
// the initial state tree.
dispatch({ type: ActionTypes.INIT } as A)

const store: Store<S & StateExt, A> & Ext = ({
const store = ({
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown) as Store<S & StateExt, A> & Ext
} as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
return store
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export {
Store,
StoreCreator,
StoreEnhancer,
StoreEnhancerStoreCreator
StoreEnhancerStoreCreator,
ExtendState
} from './types/store'
// reducers
export {
Expand Down
43 changes: 32 additions & 11 deletions src/types/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
import { Action, AnyAction } from './actions'
import { Reducer } from './reducers'

/**
* Extend the state
*
* This is used by store enhancers and store creators to extend state.
* If there is no state extension, it just returns the state, as is, otherwise
* it returns the state joined with its extension.
*
* Reference for future devs:
* https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919
*/
export type ExtendState<State, Extension> = [Extension] extends [never]
? State
: State & Extension

/**
* Internal "virtual" symbol used to make the `CombinedState` type unique.
*/
Expand Down Expand Up @@ -107,8 +121,15 @@ export type Observer<T> = {
*
* @template S The type of state held by this store.
* @template A the type of actions which may be dispatched by this store.
* @template StateExt any extension to state from store enhancers
* @template Ext any extensions to the store from store enhancers
*/
export interface Store<S = any, A extends Action = AnyAction> {
export interface Store<
S = any,
A extends Action = AnyAction,
StateExt = never,
Ext = {}
> {
/**
* Dispatches an action. It is the only way to trigger a state change.
*
Expand Down Expand Up @@ -179,9 +200,9 @@ export interface Store<S = any, A extends Action = AnyAction> {
*
* @param nextReducer The reducer for the store to use instead.
*/
replaceReducer<NewState = S, NewActions extends A = A>(
replaceReducer<NewState, NewActions extends Action>(
nextReducer: Reducer<NewState, NewActions>
): Store<NewState, NewActions>
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext

/**
* Interoperability point for observable/reactive libraries.
Expand All @@ -204,15 +225,15 @@ export interface Store<S = any, A extends Action = AnyAction> {
* @template StateExt State extension that is mixed into the state type.
*/
export interface StoreCreator {
<S, A extends Action, Ext, StateExt>(
<S, A extends Action, Ext = {}, StateExt = never>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S & StateExt, A> & Ext
<S, A extends Action, Ext, StateExt>(
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
<S, A extends Action, Ext = {}, StateExt = never>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext>
): Store<S & StateExt, A> & Ext
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
}

/**
Expand All @@ -236,13 +257,13 @@ export interface StoreCreator {
* @template Ext Store extension that is mixed into the Store type.
* @template StateExt State extension that is mixed into the state type.
*/
export type StoreEnhancer<Ext = {}, StateExt = {}> = (
next: StoreEnhancerStoreCreator
export type StoreEnhancer<Ext = {}, StateExt = never> = (
next: StoreEnhancerStoreCreator<Ext, StateExt>
) => StoreEnhancerStoreCreator<Ext, StateExt>
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = {}> = <
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <
S = any,
A extends Action = AnyAction
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
) => Store<S & StateExt, A> & Ext
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
Loading