diff --git a/src/applyMiddleware.ts b/src/applyMiddleware.ts index 744ed27895..2f1b4752e9 100644 --- a/src/applyMiddleware.ts +++ b/src/applyMiddleware.ts @@ -1,8 +1,6 @@ import compose from './compose' import { Middleware, MiddlewareAPI } from './types/middleware' -import { AnyAction } from './types/actions' -import { StoreEnhancer, Dispatch, PreloadedState } from './types/store' -import { Reducer } from './types/reducers' +import { StoreEnhancer, Dispatch } from './types/store' /** * Creates a store enhancer that applies middleware to the dispatch method @@ -55,29 +53,25 @@ export default function applyMiddleware( export default function applyMiddleware( ...middlewares: Middleware[] ): StoreEnhancer { - return createStore => - ( - reducer: Reducer, - preloadedState?: PreloadedState - ) => { - const store = createStore(reducer, preloadedState) - let dispatch: Dispatch = () => { - throw new Error( - 'Dispatching while constructing your middleware is not allowed. ' + - 'Other middleware would not be applied to this dispatch.' - ) - } + return createStore => (reducer, preloadedState) => { + const store = createStore(reducer, preloadedState) + let dispatch: Dispatch = () => { + throw new Error( + 'Dispatching while constructing your middleware is not allowed. ' + + 'Other middleware would not be applied to this dispatch.' + ) + } - const middlewareAPI: MiddlewareAPI = { - getState: store.getState, - dispatch: (action, ...args) => dispatch(action, ...args) - } - const chain = middlewares.map(middleware => middleware(middlewareAPI)) - dispatch = compose(...chain)(store.dispatch) + const middlewareAPI: MiddlewareAPI = { + getState: store.getState, + dispatch: (action, ...args) => dispatch(action, ...args) + } + const chain = middlewares.map(middleware => middleware(middlewareAPI)) + dispatch = compose(...chain)(store.dispatch) - return { - ...store, - dispatch - } + return { + ...store, + dispatch } + } } diff --git a/src/combineReducers.ts b/src/combineReducers.ts index ee005cee88..32527c7e92 100644 --- a/src/combineReducers.ts +++ b/src/combineReducers.ts @@ -1,11 +1,11 @@ import { AnyAction, Action } from './types/actions' import { ActionFromReducersMapObject, + PreloadedStateFromReducersMapObject, Reducer, ReducersMapObject, StateFromReducersMapObject } from './types/reducers' -import { CombinedState } from './types/store' import ActionTypes from './utils/actionTypes' import isPlainObject from './utils/isPlainObject' @@ -111,18 +111,32 @@ function assertReducerShape(reducers: ReducersMapObject) { * object, and builds a state object with the same shape. */ export default function combineReducers( - reducers: ReducersMapObject -): Reducer> -export default function combineReducers( - reducers: ReducersMapObject -): Reducer, A> + reducers: ReducersMapObject +): Reducer> +export default function combineReducers( + reducers: ReducersMapObject +): Reducer> +export default function combineReducers< + S, + A extends Action = AnyAction, + PreloadedState = S +>( + reducers: ReducersMapObject +): Reducer> export default function combineReducers( reducers: M ): Reducer< - CombinedState>, - ActionFromReducersMapObject + StateFromReducersMapObject, + ActionFromReducersMapObject, + Partial> > -export default function combineReducers(reducers: ReducersMapObject) { +export default function combineReducers( + reducers: M +): Reducer< + StateFromReducersMapObject, + ActionFromReducersMapObject, + Partial> +> { const reducerKeys = Object.keys(reducers) const finalReducers: ReducersMapObject = {} for (let i = 0; i < reducerKeys.length; i++) { @@ -155,7 +169,9 @@ export default function combineReducers(reducers: ReducersMapObject) { } return function combination( - state: StateFromReducersMapObject = {}, + state: + | StateFromReducersMapObject + | Partial> = {}, action: AnyAction ) { if (shapeAssertionError) { @@ -175,7 +191,7 @@ export default function combineReducers(reducers: ReducersMapObject) { } let hasChanged = false - const nextState: StateFromReducersMapObject = {} + const nextState: Partial> = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] @@ -191,11 +207,11 @@ export default function combineReducers(reducers: ReducersMapObject) { `If you want this reducer to hold no value, you can return null instead of undefined.` ) } - nextState[key] = nextStateForKey + nextState[key as keyof typeof nextState] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length - return hasChanged ? nextState : state + return (hasChanged ? nextState : state) as StateFromReducersMapObject } } diff --git a/src/createStore.ts b/src/createStore.ts index 921b723c5f..db1361c459 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -2,7 +2,6 @@ import $$observable from './utils/symbol-observable' import { Store, - PreloadedState, StoreEnhancer, Dispatch, Observer, @@ -76,21 +75,23 @@ export function createStore< export function createStore< S, A extends Action, + PreloadedState, Ext extends {} = {}, StateExt extends {} = {} >( - reducer: Reducer, - preloadedState?: PreloadedState, + reducer: Reducer, + preloadedState?: PreloadedState | undefined, enhancer?: StoreEnhancer ): Store & Ext export function createStore< S, A extends Action, + PreloadedState, Ext extends {} = {}, StateExt extends {} = {} >( - reducer: Reducer, - preloadedState?: PreloadedState | StoreEnhancer, + reducer: Reducer, + preloadedState?: PreloadedState | StoreEnhancer | undefined, enhancer?: StoreEnhancer ): Store & Ext { if (typeof reducer !== 'function') { @@ -128,12 +129,14 @@ export function createStore< return enhancer(createStore)( reducer, - preloadedState as PreloadedState - ) as Store & Ext + preloadedState as PreloadedState | undefined + ) } let currentReducer = reducer - let currentState = preloadedState as S + let currentState: S | PreloadedState | undefined = preloadedState as + | PreloadedState + | undefined let currentListeners: Map | null = new Map() let nextListeners = currentListeners let listenerIdCounter = 0 @@ -315,7 +318,7 @@ export function createStore< ) } - currentReducer = nextReducer + currentReducer = nextReducer as unknown as Reducer // This action has a similar effect to ActionTypes.INIT. // Any reducers that existed in both the new and old rootReducer @@ -455,21 +458,23 @@ export function legacy_createStore< export function legacy_createStore< S, A extends Action, + PreloadedState, Ext extends {} = {}, StateExt extends {} = {} >( - reducer: Reducer, - preloadedState?: PreloadedState, + reducer: Reducer, + preloadedState?: PreloadedState | undefined, enhancer?: StoreEnhancer ): Store & Ext export function legacy_createStore< S, A extends Action, + PreloadedState, Ext extends {} = {}, StateExt extends {} = {} >( reducer: Reducer, - preloadedState?: PreloadedState | StoreEnhancer, + preloadedState?: PreloadedState | StoreEnhancer | undefined, enhancer?: StoreEnhancer ): Store & Ext { return createStore(reducer, preloadedState as any, enhancer) diff --git a/src/index.ts b/src/index.ts index 0fa083343b..154bde77ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,6 @@ import __DO_NOT_USE__ActionTypes from './utils/actionTypes' // types // store export { - CombinedState, - PreloadedState, Dispatch, Unsubscribe, Observable, @@ -23,9 +21,10 @@ export { // reducers export { Reducer, - ReducerFromReducersMapObject, ReducersMapObject, StateFromReducersMapObject, + PreloadedStateFromReducersMapObject, + ReducerFromReducersMapObject, ActionFromReducer, ActionFromReducersMapObject } from './types/reducers' diff --git a/src/types/reducers.ts b/src/types/reducers.ts index cd084f203a..d60d135e3f 100644 --- a/src/types/reducers.ts +++ b/src/types/reducers.ts @@ -25,19 +25,31 @@ import { Action, AnyAction } from './actions' * * @template S The type of state consumed and produced by this reducer. * @template A The type of actions the reducer can potentially respond to. + * @template PreloadedState The type of state consumed by this reducer the first time it's called. */ -export type Reducer = ( - state: S | undefined, - action: A -) => S +export type Reducer< + S = any, + A extends Action = AnyAction, + PreloadedState = S +> = (state: S | PreloadedState | undefined, action: A) => S /** * Object whose values correspond to different reducer functions. * + * @template S The combined state of the reducers. * @template A The type of actions the reducers can potentially respond to. + * @template PreloadedState The combined preloaded state of the reducers. */ -export type ReducersMapObject = { - [K in keyof S]: Reducer +export type ReducersMapObject< + S = any, + A extends Action = AnyAction, + PreloadedState = S +> = { + [K in keyof S]: Reducer< + S[K], + A, + K extends keyof PreloadedState ? PreloadedState[K] : never + > } /** @@ -45,35 +57,56 @@ export type ReducersMapObject = { * * @template M Object map of reducers as provided to `combineReducers(map: M)`. */ -export type StateFromReducersMapObject = M extends ReducersMapObject - ? { [P in keyof M]: M[P] extends Reducer ? S : never } - : never +export type StateFromReducersMapObject< + M extends ReducersMapObject +> = { + [P in keyof M]: M[P] extends Reducer | undefined + ? S + : never +} /** - * Infer reducer union type from a `ReducersMapObject`. + * Infer a combined preloaded state shape from a `ReducersMapObject`. * * @template M Object map of reducers as provided to `combineReducers(map: M)`. */ -export type ReducerFromReducersMapObject = M extends { - [P in keyof M]: infer R -} - ? R extends Reducer - ? R +export type PreloadedStateFromReducersMapObject< + M extends ReducersMapObject +> = { + [P in keyof M]: M[P] extends + | Reducer + | undefined + ? PreloadedState : never - : never +} + +/** + * Infer reducer union type from a `ReducersMapObject`. + * + * @template M Object map of reducers as provided to `combineReducers(map: M)`. + */ +export type ReducerFromReducersMapObject< + M extends ReducersMapObject +> = M[keyof M] /** * Infer action type from a reducer function. * * @template R Type of reducer. */ -export type ActionFromReducer = R extends Reducer ? A : never +export type ActionFromReducer> = R extends Reducer< + any, + infer A, + any +> + ? A + : never /** * Infer action union type from a `ReducersMapObject`. * * @template M Object map of reducers as provided to `combineReducers(map: M)`. */ -export type ActionFromReducersMapObject = M extends ReducersMapObject - ? ActionFromReducer> - : never +export type ActionFromReducersMapObject< + M extends ReducersMapObject +> = ActionFromReducer> diff --git a/src/types/store.ts b/src/types/store.ts index f071dcb136..3021ff09e1 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -2,49 +2,6 @@ import { Action, AnyAction } from './actions' import { Reducer } from './reducers' import '../utils/symbol-observable' -/** - * Internal "virtual" symbol used to make the `CombinedState` type unique. - */ -declare const $CombinedState: unique symbol - -/** - * State base type for reducers created with `combineReducers()`. - * - * This type allows the `createStore()` method to infer which levels of the - * preloaded state can be partial. - * - * Because Typescript is really duck-typed, a type needs to have some - * identifying property to differentiate it from other types with matching - * prototypes for type checking purposes. That's why this type has the - * `$CombinedState` symbol property. Without the property, this type would - * match any object. The symbol doesn't really exist because it's an internal - * (i.e. not exported), and internally we never check its value. Since it's a - * symbol property, it's not expected to be unumerable, and the value is - * typed as always undefined, so its never expected to have a meaningful - * value anyway. It just makes this type distinguishable from plain `{}`. - */ -interface EmptyObject { - readonly [$CombinedState]?: undefined -} -export type CombinedState = EmptyObject & S - -/** - * Recursively makes combined state objects partial. Only combined state _root - * objects_ (i.e. the generated higher level object with keys mapping to - * individual reducers) are partial. - */ -export type PreloadedState = Required extends EmptyObject - ? S extends CombinedState - ? { - [K in keyof S1]?: S1[K] extends object ? PreloadedState : S1[K] - } - : S - : { - [K in keyof S]: S[K] extends string | number | boolean | symbol - ? S[K] - : PreloadedState - } - /** * A *dispatching function* (or simply *dispatch function*) is a function that * accepts an action or an async action; it then may or may not dispatch one @@ -214,6 +171,7 @@ export interface Store< * * @template S The type of state to be held by the store. * @template A The type of actions which may be dispatched. + * @template PreloadedState The initial state that is passed into the reducer. * @template Ext Store extension that is mixed in to the Store type. * @template StateExt State extension that is mixed into the state type. */ @@ -222,9 +180,15 @@ export interface StoreCreator { reducer: Reducer, enhancer?: StoreEnhancer ): Store & Ext - ( - reducer: Reducer, - preloadedState?: PreloadedState, + < + S, + A extends Action, + PreloadedState, + Ext extends {} = {}, + StateExt extends {} = {} + >( + reducer: Reducer, + preloadedState?: PreloadedState | undefined, enhancer?: StoreEnhancer ): Store & Ext } @@ -259,7 +223,7 @@ export type StoreEnhancer = < export type StoreEnhancerStoreCreator< Ext extends {} = {}, StateExt extends {} = {} -> = ( - reducer: Reducer, - preloadedState?: PreloadedState +> = ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => Store & Ext diff --git a/test/createStore.spec.ts b/test/createStore.spec.ts index ec708b8d76..b19758383a 100644 --- a/test/createStore.spec.ts +++ b/test/createStore.spec.ts @@ -847,7 +847,7 @@ describe('createStore', () => { const store = createStore( combineReducers<{ x?: number; y: { z: number; w?: number } }>({ x: (s = 0, _) => s, - y: combineReducers({ + y: combineReducers<{ z: number; w?: number }>({ z: (s = 0, _) => s, w: (s = 0, _) => s }) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 282d5166ed..9771d5ad68 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -15,9 +15,9 @@ function dispatchExtension() { dispatch: PromiseDispatch }> = createStore => - ( - reducer: Reducer, - preloadedState?: any + ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { const store = createStore(reducer, preloadedState) return { @@ -48,16 +48,18 @@ function dispatchExtension() { */ function stateExtension() { interface ExtraState { - extraField: 'extra' + extraField: string } const enhancer: StoreEnhancer<{}, ExtraState> = createStore => - ( - reducer: Reducer, - preloadedState?: any + ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { - function wrapReducer(reducer: Reducer): Reducer { + function wrapReducer( + reducer: Reducer + ): Reducer { return (state, action) => { const newState = reducer(state, action) return { @@ -116,16 +118,18 @@ function extraMethods() { */ function replaceReducerExtender() { interface ExtraState { - extraField: 'extra' + extraField: string } const enhancer: StoreEnhancer<{ method(): string }, ExtraState> = createStore => - ( - reducer: Reducer, - preloadedState?: any + ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { - function wrapReducer(reducer: Reducer): Reducer { + function wrapReducer( + reducer: Reducer + ): Reducer { return (state, action) => { const newState = reducer(state, action) return { @@ -192,16 +196,20 @@ function mhelmersonExample() { function stateExtensionExpectedToWork() { interface ExtraState { - extraField: 'extra' + extraField: string } const enhancer: StoreEnhancer<{}, ExtraState> = createStore => - ( - reducer: Reducer, - preloadedState?: any + ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { - const wrappedReducer: Reducer = (state, action) => { + const wrappedReducer: Reducer< + S & ExtraState, + A, + PreloadedState & ExtraState + > = (state, action) => { const newState = reducer(state, action) return { ...newState, @@ -268,11 +276,14 @@ function finalHelmersonExample() { foo: string } - function persistReducer>( + function persistReducer, PreloadedState>( config: any, - reducer: Reducer + reducer: Reducer ) { - return (state: (S & ExtraState) | undefined, action: A) => { + return ( + state: (S & ExtraState) | PreloadedState | undefined, + action: A + ) => { const newState = reducer(state, action) return { ...newState, @@ -289,11 +300,11 @@ function finalHelmersonExample() { persistConfig: any ): StoreEnhancer<{}, ExtraState> { return createStore => - >( - reducer: Reducer, - preloadedState?: any + , PreloadedState>( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { - const persistedReducer = persistReducer(persistConfig, reducer) + const persistedReducer = persistReducer(persistConfig, reducer) const store = createStore(persistedReducer, preloadedState) const persistor = persistStore(store)