From 2fcea31a8dd0f616905fae4f5b36a286757bb810 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 23 Aug 2019 21:37:18 -0400 Subject: [PATCH 01/21] fix replaceReducer with a store enhancer --- index.d.ts | 24 ++++++++++++++++-------- test/typescript/enhancers.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/index.d.ts b/index.d.ts index db64dd5e3c..e7da28e60e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -254,8 +254,15 @@ export type Observer = { * * @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 { +export interface Store< + S = any, + A extends Action = AnyAction, + StateExt = {}, + Ext = {} +> { /** * Dispatches an action. It is the only way to trigger a state change. * @@ -328,7 +335,7 @@ export interface Store { */ replaceReducer( nextReducer: Reducer - ): Store + ): Store & Ext /** * Interoperability point for observable/reactive libraries. @@ -355,15 +362,15 @@ export type DeepPartial = { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - ( + ( reducer: Reducer, enhancer?: StoreEnhancer - ): Store & Ext - ( + ): Store & Ext + ( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer - ): Store & Ext + ): Store & Ext } /** @@ -418,15 +425,16 @@ export const createStore: StoreCreator * @template StateExt State extension that is mixed into the state type. */ export type StoreEnhancer = ( - next: StoreEnhancerStoreCreator + next: StoreEnhancerStoreCreator ) => StoreEnhancerStoreCreator + export type StoreEnhancerStoreCreator = < S = any, A extends Action = AnyAction >( reducer: Reducer, preloadedState?: PreloadedState -) => Store & Ext +) => Store & Ext /* middleware */ diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 5e8e180bd9..1c3513709c 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -71,3 +71,37 @@ function extraMethods() { // typings:expect-error store.wrongMethod() } + +/** + * replaceReducer with a store enhancer + */ +function replaceReducerExtender() { + interface ExtraState { + extraField: 'extra' + } + + const enhancer: StoreEnhancer<{}, ExtraState> = createStore => < + S, + A extends Action = AnyAction + >( + reducer: Reducer, + preloadedState?: PreloadedState + ) => { + const wrappedReducer: Reducer = null as any + const wrappedPreloadedState: PreloadedState = null as any + return createStore(wrappedReducer, wrappedPreloadedState) + } + + const store = createStore(reducer, enhancer) + + const newReducer = ( + state: { test: boolean } = { test: true }, + _: AnyAction + ) => state + + const newStore = store.replaceReducer(newReducer) + newStore.getState().test + store.getState().extraField + // typings:expect-error + store.getState().wrongField +} From cacaecd77c7c238912789b8c1d36df08b359972d Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 23 Aug 2019 21:42:03 -0400 Subject: [PATCH 02/21] remove erroneous restriction on StateExt --- index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index e7da28e60e..efc1ffecb8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -362,11 +362,11 @@ export type DeepPartial = { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - ( + ( reducer: Reducer, enhancer?: StoreEnhancer ): Store & Ext - ( + ( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer From 115e78c469ae5d4488eb3d22af922e56bcf1d176 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 23 Aug 2019 21:46:34 -0400 Subject: [PATCH 03/21] remove the other extension - our store enhancer might add array functionality, for instance --- index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index efc1ffecb8..a4f1409ea5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -362,11 +362,11 @@ export type DeepPartial = { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - ( + ( reducer: Reducer, enhancer?: StoreEnhancer ): Store & Ext - ( + ( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer From 5b2957a9f5a25413027a0bb416dcd1cfc8153b96 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 23 Aug 2019 21:48:40 -0400 Subject: [PATCH 04/21] add reasonable defaults for Ext and StateExt --- index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index a4f1409ea5..c5ef392f5f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -362,11 +362,11 @@ export type DeepPartial = { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - ( + ( reducer: Reducer, enhancer?: StoreEnhancer ): Store & Ext - ( + ( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer From 7d8acc227749759e09b545a3df8eecf6168ed8f3 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 23 Aug 2019 21:52:17 -0400 Subject: [PATCH 05/21] fix state, add a test for non-object-based state --- index.d.ts | 6 +++--- test/typescript/store.ts | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index c5ef392f5f..36ee6ef58b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -260,7 +260,7 @@ export type Observer = { export interface Store< S = any, A extends Action = AnyAction, - StateExt = {}, + StateExt = S, Ext = {} > { /** @@ -362,11 +362,11 @@ export type DeepPartial = { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - ( + ( reducer: Reducer, enhancer?: StoreEnhancer ): Store & Ext - ( + ( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer diff --git a/test/typescript/store.ts b/test/typescript/store.ts index 123464ce55..daee637600 100644 --- a/test/typescript/store.ts +++ b/test/typescript/store.ts @@ -56,6 +56,9 @@ const funcWithStore = (store: Store) => {} const store: Store = createStore(reducer) +// ensure that an array-based state works +const arrayReducer = (state = []) => state +const storeWithArrayState: Store<[]> = createStore(arrayReducer) const storeWithPreloadedState: Store = createStore(reducer, { a: 'a', b: { c: 'c', d: 'd' } From 1d0d11af34524928c130263596b00e3b1a374c0b Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 23 Aug 2019 22:00:23 -0400 Subject: [PATCH 06/21] add verification that store extension is also passed to replaceReducer --- test/typescript/enhancers.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 1c3513709c..7635ca424d 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -80,10 +80,10 @@ function replaceReducerExtender() { extraField: 'extra' } - const enhancer: StoreEnhancer<{}, ExtraState> = createStore => < - S, - A extends Action = AnyAction - >( + const enhancer: StoreEnhancer< + { method(): string }, + ExtraState + > = createStore => ( reducer: Reducer, preloadedState?: PreloadedState ) => { @@ -104,4 +104,8 @@ function replaceReducerExtender() { store.getState().extraField // typings:expect-error store.getState().wrongField + + const res: string = store.method() + // typings:expect-error + store.wrongMethod() } From 554b79e31ecd44237eb530261cf8cff0b9507548 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 23 Aug 2019 22:06:42 -0400 Subject: [PATCH 07/21] better fix: set state default based on what base type it is --- index.d.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 36ee6ef58b..9788aeb398 100644 --- a/index.d.ts +++ b/index.d.ts @@ -260,7 +260,7 @@ export type Observer = { export interface Store< S = any, A extends Action = AnyAction, - StateExt = S, + StateExt = S extends {} ? {} : S extends [] ? [] : S, Ext = {} > { /** @@ -362,11 +362,21 @@ export type DeepPartial = { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - ( + < + S, + A extends Action, + Ext = {}, + StateExt = S extends {} ? {} : S extends [] ? [] : S + >( reducer: Reducer, enhancer?: StoreEnhancer ): Store & Ext - ( + < + S, + A extends Action, + Ext = {}, + StateExt = S extends {} ? {} : S extends [] ? [] : S + >( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer From 4713921c3e87f12d6a645e33f14523d7ba892e5f Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 23 Aug 2019 22:10:11 -0400 Subject: [PATCH 08/21] fix array test --- test/typescript/store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/typescript/store.ts b/test/typescript/store.ts index daee637600..0ec113379a 100644 --- a/test/typescript/store.ts +++ b/test/typescript/store.ts @@ -57,8 +57,8 @@ const funcWithStore = (store: Store) => {} const store: Store = createStore(reducer) // ensure that an array-based state works -const arrayReducer = (state = []) => state -const storeWithArrayState: Store<[]> = createStore(arrayReducer) +const arrayReducer = (state: any[] = []) => state || [] +const storeWithArrayState: Store = createStore(arrayReducer) const storeWithPreloadedState: Store = createStore(reducer, { a: 'a', b: { c: 'c', d: 'd' } From 7168df6d341ed25afd44967ea7c04ad679aaf720 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 23 Aug 2019 22:29:13 -0400 Subject: [PATCH 09/21] fix typing of StateExt --- index.d.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/index.d.ts b/index.d.ts index 9788aeb398..94c24242ea 100644 --- a/index.d.ts +++ b/index.d.ts @@ -247,6 +247,15 @@ export type Observer = { next?(value: T): void } +/** + * returns the most basic type that will not interfere with the existing type + * + * Note that for non-object stuff, this will mess with replaceReducer. The + * assumption is that root reducers that only return a basic type (string, number, null, symbol) probably won't + * be using replaceReducer anyways. + */ +export type BaseType = S extends {} ? {} : S + /** * 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 @@ -260,7 +269,7 @@ export type Observer = { export interface Store< S = any, A extends Action = AnyAction, - StateExt = S extends {} ? {} : S extends [] ? [] : S, + StateExt = BaseType, Ext = {} > { /** @@ -362,21 +371,11 @@ export type DeepPartial = { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - < - S, - A extends Action, - Ext = {}, - StateExt = S extends {} ? {} : S extends [] ? [] : S - >( + >( reducer: Reducer, enhancer?: StoreEnhancer ): Store & Ext - < - S, - A extends Action, - Ext = {}, - StateExt = S extends {} ? {} : S extends [] ? [] : S - >( + >( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer From 66b609074c8ccc1b1774d714b13d290a7770c35c Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Wed, 28 Aug 2019 17:04:19 -0400 Subject: [PATCH 10/21] add mhelmerson example --- test/typescript/enhancers.ts | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 7635ca424d..717c82e17c 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -109,3 +109,43 @@ function replaceReducerExtender() { // typings:expect-error store.wrongMethod() } + +function mhelmersonExample() { + interface State { + someField: 'string' + } + + interface ExtraState { + extraField: 'extra' + } + + const reducer: Reducer = null as any + + function stateExtensionExpectedToWork() { + interface ExtraState { + extraField: 'extra' + } + + const enhancer: StoreEnhancer<{}, ExtraState> = createStore => < + S, + A extends Action = AnyAction + >( + reducer: Reducer, + preloadedState?: PreloadedState + ) => { + const wrappedReducer: Reducer = null as any + const wrappedPreloadedState: PreloadedState = null as any + const store = createStore(wrappedReducer, wrappedPreloadedState) + return { + ...store, + replaceReducer: (nextReducer: Reducer) => { + const nextWrappedReducer: Reducer = null as any + return store.replaceReducer(nextWrappedReducer) + } + } as Store + } + + const store = createStore(reducer, enhancer) + store.replaceReducer(reducer) + } +} From 6e60f2dad19efa4dbb1c504de5c1bc18588e142a Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Thu, 29 Aug 2019 10:16:31 -0400 Subject: [PATCH 11/21] fix replaceReducer, so that it infers types, fix example test --- index.d.ts | 2 +- test/typescript/enhancers.ts | 37 ++++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index 94c24242ea..110cbb5d75 100644 --- a/index.d.ts +++ b/index.d.ts @@ -342,7 +342,7 @@ export interface Store< * * @param nextReducer The reducer for the store to use instead. */ - replaceReducer( + replaceReducer( nextReducer: Reducer ): Store & Ext diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 717c82e17c..8bcd482a59 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -131,18 +131,43 @@ function mhelmersonExample() { A extends Action = AnyAction >( reducer: Reducer, - preloadedState?: PreloadedState + preloadedState?: any ) => { - const wrappedReducer: Reducer = null as any - const wrappedPreloadedState: PreloadedState = null as any + const wrappedReducer = (state: S & ExtraState | undefined, action: A) => { + const newState = reducer(state, action) + return { + ...newState, + extraField: 'extra' + } as S & ExtraState + } + const wrappedPreloadedState = preloadedState + ? { + ...preloadedState, + extraField: 'extra' + } + : undefined const store = createStore(wrappedReducer, wrappedPreloadedState) return { ...store, - replaceReducer: (nextReducer: Reducer) => { - const nextWrappedReducer: Reducer = null as any + replaceReducer( + nextReducer: ( + state: NS & ExtraState | undefined, + action: NA + ) => NS & ExtraState + ) { + const nextWrappedReducer: Reducer = ( + state, + action + ) => { + const newState = nextReducer(state, action) + return { + ...newState, + extraField: 'extra' + } + } return store.replaceReducer(nextWrappedReducer) } - } as Store + } } const store = createStore(reducer, enhancer) From 2a5acb6e9f4496a902f5937c7d7708ad63c56029 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Thu, 29 Aug 2019 10:27:09 -0400 Subject: [PATCH 12/21] fix the weird type hacks in the test --- test/typescript/enhancers.ts | 58 ++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 8bcd482a59..2711f8ca85 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -18,7 +18,25 @@ const reducer: Reducer = null as any function dispatchExtension() { type PromiseDispatch = (promise: Promise) => Promise - const enhancer: StoreEnhancer<{ dispatch: PromiseDispatch }> = null as any + const enhancer: StoreEnhancer<{ + dispatch: PromiseDispatch + }> = createStore => ( + reducer: Reducer, + preloadedState?: any + ) => { + const store = createStore(reducer, preloadedState) + return { + ...store, + dispatch: (action: any) => { + if (action.type) { + store.dispatch(action) + } else if (action.then) { + action.then(store.dispatch) + } + return action + } + } + } const store = createStore(reducer, enhancer) @@ -43,10 +61,21 @@ function stateExtension() { A extends Action = AnyAction >( reducer: Reducer, - preloadedState?: PreloadedState + preloadedState?: any ) => { - const wrappedReducer: Reducer = null as any - const wrappedPreloadedState: PreloadedState = null as any + const wrappedReducer: Reducer = (state, action) => { + const newState = reducer(state, action) + return { + ...newState, + extraField: 'extra' + } + } + const wrappedPreloadedState = preloadedState + ? { + ...preloadedState, + extraField: 'extra' + } + : undefined return createStore(wrappedReducer, wrappedPreloadedState) } @@ -85,10 +114,21 @@ function replaceReducerExtender() { ExtraState > = createStore => ( reducer: Reducer, - preloadedState?: PreloadedState + preloadedState?: any ) => { - const wrappedReducer: Reducer = null as any - const wrappedPreloadedState: PreloadedState = null as any + const wrappedReducer: Reducer = (state, action) => { + const newState = reducer(state, action) + return { + ...newState, + extraField: 'extra' + } + } + const wrappedPreloadedState = preloadedState + ? { + ...preloadedState, + extraField: 'extra' + } + : undefined return createStore(wrappedReducer, wrappedPreloadedState) } @@ -133,12 +173,12 @@ function mhelmersonExample() { reducer: Reducer, preloadedState?: any ) => { - const wrappedReducer = (state: S & ExtraState | undefined, action: A) => { + const wrappedReducer: Reducer = (state, action) => { const newState = reducer(state, action) return { ...newState, extraField: 'extra' - } as S & ExtraState + } } const wrappedPreloadedState = preloadedState ? { From 3f8cfb526b50c791a6e5cd9992a000090a9f3f1b Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Thu, 29 Aug 2019 10:34:04 -0400 Subject: [PATCH 13/21] add final working example --- test/typescript/enhancers.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 2711f8ca85..759f11f91c 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -214,3 +214,34 @@ function mhelmersonExample() { store.replaceReducer(reducer) } } + +function finalHelmersonExample() { + function persistReducer(config: any, reducer: S): S { + return reducer + } + + function persistStore(store: S) { + return store + } + + function createPersistEnhancer(persistConfig: any): StoreEnhancer { + return createStore => ( + reducer: Reducer, + preloadedState?: any + ) => { + const persistedReducer = persistReducer(persistConfig, reducer) + const store = createStore(persistedReducer, preloadedState) + const persistor = persistStore(store) + + return { + ...store, + replaceReducer: nextReducer => { + return store.replaceReducer( + persistReducer(persistConfig, nextReducer) + ) + }, + persistor + } + } + } +} From 806f2aa27000e103827adc0f482a0e0f31215624 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Thu, 29 Aug 2019 16:59:56 -0400 Subject: [PATCH 14/21] update based on PR type changes --- src/createStore.ts | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/createStore.ts b/src/createStore.ts index f6ab9e2cf2..1f98b4b231 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -5,7 +5,8 @@ import { PreloadedState, StoreEnhancer, Dispatch, - Observer + Observer, + BaseType } from './types/store' import { Action } from './types/actions' import { Reducer } from './types/reducers' @@ -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( +export default function createStore< + S, + A extends Action, + Ext = {}, + StateExt = BaseType +>( reducer: Reducer, enhancer?: StoreEnhancer -): Store & Ext -export default function createStore( +): Store & Ext +export default function createStore< + S, + A extends Action, + Ext = {}, + StateExt = BaseType +>( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer -): Store & Ext -export default function createStore( +): Store & Ext +export default function createStore< + S, + A extends Action, + Ext = {}, + StateExt = BaseType +>( reducer: Reducer, preloadedState?: PreloadedState | StoreEnhancer, - enhancer?: StoreEnhancer -): Store & Ext { + enhancer?: StoreEnhancer +): Store & Ext { if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') From c736cf3011719165362b29b0fcb2a8eedfb402a3 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Thu, 29 Aug 2019 17:03:13 -0400 Subject: [PATCH 15/21] fix type --- src/createStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/createStore.ts b/src/createStore.ts index 1f98b4b231..a21be5c8bc 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -333,12 +333,12 @@ export default function createStore< // the initial state tree. dispatch({ type: ActionTypes.INIT } as A) - const store: Store & Ext = ({ + const store = ({ dispatch: dispatch as Dispatch, subscribe, getState, replaceReducer, [$$observable]: observable - } as unknown) as Store & Ext + } as unknown) as Store & Ext return store } From bbfb018e8bab714f82daeae81e31a0ad5233a8e2 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 30 Aug 2019 10:15:42 -0400 Subject: [PATCH 16/21] update tests to reflect complete examples --- test/typescript/enhancers.ts | 66 +++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 759f11f91c..e39ca83628 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -141,13 +141,13 @@ function replaceReducerExtender() { const newStore = store.replaceReducer(newReducer) newStore.getState().test - store.getState().extraField + newStore.getState().extraField // typings:expect-error - store.getState().wrongField + newStore.getState().wrongField - const res: string = store.method() + const res: string = newStore.method() // typings:expect-error - store.wrongMethod() + newStore.wrongMethod() } function mhelmersonExample() { @@ -212,24 +212,56 @@ function mhelmersonExample() { const store = createStore(reducer, enhancer) store.replaceReducer(reducer) + + store.getState().extraField + // typings:expect-error + store.getState().wrongField + // typings:expect-error + store.getState().test + + const newReducer = ( + state: { test: boolean } = { test: true }, + _: AnyAction + ) => state + + const newStore = store.replaceReducer(newReducer) + newStore.getState().test + newStore.getState().extraField + // typings:expect-error + newStore.getState().wrongField } } function finalHelmersonExample() { - function persistReducer(config: any, reducer: S): S { - return reducer + interface ExtraState { + foo: string + } + + function persistReducer( + config: any, + reducer: Reducer + ) { + return (state: S & ExtraState | undefined, action: AnyAction) => { + const newState = reducer(state, (action as unknown) as A) + return { + ...newState, + foo: 'hi' + } + } } function persistStore(store: S) { return store } - function createPersistEnhancer(persistConfig: any): StoreEnhancer { + function createPersistEnhancer( + persistConfig: any + ): StoreEnhancer<{}, ExtraState> { return createStore => ( reducer: Reducer, preloadedState?: any ) => { - const persistedReducer = persistReducer(persistConfig, reducer) + const persistedReducer = persistReducer(persistConfig, reducer) const store = createStore(persistedReducer, preloadedState) const persistor = persistStore(store) @@ -244,4 +276,22 @@ function finalHelmersonExample() { } } } + + const store = createStore(reducer, createPersistEnhancer('hi')) + + store.getState().foo + // typings:expect-error + store.getState().wrongField + + const newReducer = ( + state: { test: boolean } = { test: true }, + _: AnyAction + ) => state + + const newStore = store.replaceReducer(newReducer) + newStore.getState().test + // typings:expect-error + newStore.getState().whatever + // typings:expect-error + newStore.getState().wrongField } From f5f796c716f4686bd70de415c3ceca498a000824 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Fri, 30 Aug 2019 14:26:00 -0400 Subject: [PATCH 17/21] merge the changes from index.d.ts into types/store.ts --- src/types/store.ts | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/types/store.ts b/src/types/store.ts index 688b46b6c9..3ce5fb3427 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -2,6 +2,15 @@ import { Action, AnyAction } from './actions' import { Reducer } from './reducers' +/** + * returns the most basic type that will not interfere with the existing type + * + * Note that for non-object stuff, this will mess with replaceReducer. The + * assumption is that root reducers that only return a basic type (string, number, null, symbol) probably won't + * be using replaceReducer anyways. + */ +export type BaseType = S extends {} ? {} : S + /** * Internal "virtual" symbol used to make the `CombinedState` type unique. */ @@ -107,8 +116,15 @@ export type Observer = { * * @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 { +export interface Store< + S = any, + A extends Action = AnyAction, + StateExt = BaseType, + Ext = {} +> { /** * Dispatches an action. It is the only way to trigger a state change. * @@ -179,9 +195,9 @@ export interface Store { * * @param nextReducer The reducer for the store to use instead. */ - replaceReducer( + replaceReducer( nextReducer: Reducer - ): Store + ): Store & Ext /** * Interoperability point for observable/reactive libraries. @@ -204,15 +220,15 @@ export interface Store { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - ( + >( reducer: Reducer, enhancer?: StoreEnhancer - ): Store & Ext - ( + ): Store & Ext + >( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer - ): Store & Ext + ): Store & Ext } /** @@ -237,7 +253,7 @@ export interface StoreCreator { * @template StateExt State extension that is mixed into the state type. */ export type StoreEnhancer = ( - next: StoreEnhancerStoreCreator + next: StoreEnhancerStoreCreator ) => StoreEnhancerStoreCreator export type StoreEnhancerStoreCreator = < S = any, @@ -245,4 +261,4 @@ export type StoreEnhancerStoreCreator = < >( reducer: Reducer, preloadedState?: PreloadedState -) => Store & Ext +) => Store & Ext From b95330c1e782970745f64a4bc49ad5761efb6785 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Mon, 2 Sep 2019 20:37:40 -0400 Subject: [PATCH 18/21] extend store type --- src/createStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/createStore.ts b/src/createStore.ts index a21be5c8bc..6e19378d51 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -90,7 +90,7 @@ export default function createStore< return enhancer(createStore)(reducer, preloadedState as PreloadedState< S - >) as Store & Ext + >) as Store & Ext } if (typeof reducer !== 'function') { From b3ae3d53955a5883034b39444c808c739db8f611 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Tue, 3 Sep 2019 13:52:55 -0400 Subject: [PATCH 19/21] much better approach: only extend the state when we have an extension --- index.d.ts | 30 ++++++++++++++++-------------- src/createStore.ts | 28 +++++++++++++++++----------- src/index.ts | 3 ++- src/types/store.ts | 26 ++++++++++++++------------ test/typescript/store.ts | 38 +++++++++++++++++++++++++++++++++++++- 5 files changed, 86 insertions(+), 39 deletions(-) diff --git a/index.d.ts b/index.d.ts index 110cbb5d75..aa8ec2ad31 100644 --- a/index.d.ts +++ b/index.d.ts @@ -248,13 +248,15 @@ export type Observer = { } /** - * returns the most basic type that will not interfere with the existing type + * Extend the state * - * Note that for non-object stuff, this will mess with replaceReducer. The - * assumption is that root reducers that only return a basic type (string, number, null, symbol) probably won't - * be using replaceReducer anyways. + * 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 BaseType = S extends {} ? {} : S +export type ExtendState = [Extension] extends [never] + ? State + : State & Extension /** * A store is an object that holds the application's state tree. @@ -269,7 +271,7 @@ export type BaseType = S extends {} ? {} : S export interface Store< S = any, A extends Action = AnyAction, - StateExt = BaseType, + StateExt = never, Ext = {} > { /** @@ -344,7 +346,7 @@ export interface Store< */ replaceReducer( nextReducer: Reducer - ): Store & Ext + ): Store, NewActions, StateExt, Ext> & Ext /** * Interoperability point for observable/reactive libraries. @@ -371,15 +373,15 @@ export type DeepPartial = { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - >( + ( reducer: Reducer, enhancer?: StoreEnhancer - ): Store & Ext - >( + ): Store, A, StateExt, Ext> & Ext + ( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer - ): Store & Ext + ): Store, A, StateExt, Ext> & Ext } /** @@ -433,17 +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 = ( +export type StoreEnhancer = ( next: StoreEnhancerStoreCreator ) => StoreEnhancerStoreCreator -export type StoreEnhancerStoreCreator = < +export type StoreEnhancerStoreCreator = < S = any, A extends Action = AnyAction >( reducer: Reducer, preloadedState?: PreloadedState -) => Store & Ext +) => Store, A, StateExt, Ext> & Ext /* middleware */ diff --git a/src/createStore.ts b/src/createStore.ts index 6e19378d51..b6c2439414 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -6,7 +6,7 @@ import { StoreEnhancer, Dispatch, Observer, - BaseType + ExtendState } from './types/store' import { Action } from './types/actions' import { Reducer } from './types/reducers' @@ -42,31 +42,31 @@ export default function createStore< S, A extends Action, Ext = {}, - StateExt = BaseType + StateExt = never >( reducer: Reducer, enhancer?: StoreEnhancer -): Store & Ext +): Store, A, StateExt, Ext> & Ext export default function createStore< S, A extends Action, Ext = {}, - StateExt = BaseType + StateExt = never >( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer -): Store & Ext +): Store, A, StateExt, Ext> & Ext export default function createStore< S, A extends Action, Ext = {}, - StateExt = BaseType + StateExt = never >( reducer: Reducer, preloadedState?: PreloadedState | StoreEnhancer, enhancer?: StoreEnhancer -): Store & Ext { +): Store, A, StateExt, Ext> & Ext { if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') @@ -90,7 +90,7 @@ export default function createStore< return enhancer(createStore)(reducer, preloadedState as PreloadedState< S - >) as Store & Ext + >) as Store, A, StateExt, Ext> & Ext } if (typeof reducer !== 'function') { @@ -268,7 +268,7 @@ export default function createStore< */ function replaceReducer( nextReducer: Reducer - ): Store { + ): Store, NewActions, StateExt, Ext> & Ext { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } @@ -285,7 +285,13 @@ export default function createStore< // 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 + return (store as unknown) as Store< + ExtendState, + NewActions, + StateExt, + Ext + > & + Ext } /** @@ -339,6 +345,6 @@ export default function createStore< getState, replaceReducer, [$$observable]: observable - } as unknown) as Store & Ext + } as unknown) as Store, A, StateExt, Ext> & Ext return store } diff --git a/src/index.ts b/src/index.ts index 350d1f61eb..40df5f346d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,8 @@ export { Store, StoreCreator, StoreEnhancer, - StoreEnhancerStoreCreator + StoreEnhancerStoreCreator, + ExtendState } from './types/store' // reducers export { diff --git a/src/types/store.ts b/src/types/store.ts index 3ce5fb3427..09a3585dd0 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -3,13 +3,15 @@ import { Action, AnyAction } from './actions' import { Reducer } from './reducers' /** - * returns the most basic type that will not interfere with the existing type + * Extend the state * - * Note that for non-object stuff, this will mess with replaceReducer. The - * assumption is that root reducers that only return a basic type (string, number, null, symbol) probably won't - * be using replaceReducer anyways. + * 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 BaseType = S extends {} ? {} : S +export type ExtendState = [Extension] extends [never] + ? State + : State & Extension /** * Internal "virtual" symbol used to make the `CombinedState` type unique. @@ -122,7 +124,7 @@ export type Observer = { export interface Store< S = any, A extends Action = AnyAction, - StateExt = BaseType, + StateExt = never, Ext = {} > { /** @@ -197,7 +199,7 @@ export interface Store< */ replaceReducer( nextReducer: Reducer - ): Store & Ext + ): Store, NewActions, StateExt, Ext> & Ext /** * Interoperability point for observable/reactive libraries. @@ -220,15 +222,15 @@ export interface Store< * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - >( + ( reducer: Reducer, enhancer?: StoreEnhancer - ): Store & Ext - >( + ): Store, A, StateExt, Ext> & Ext + ( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer - ): Store & Ext + ): Store, A, StateExt, Ext> & Ext } /** @@ -261,4 +263,4 @@ export type StoreEnhancerStoreCreator = < >( reducer: Reducer, preloadedState?: PreloadedState -) => Store & Ext +) => Store, A, StateExt, Ext> & Ext diff --git a/test/typescript/store.ts b/test/typescript/store.ts index 0ec113379a..b387c0b15c 100644 --- a/test/typescript/store.ts +++ b/test/typescript/store.ts @@ -7,7 +7,8 @@ import { StoreCreator, StoreEnhancerStoreCreator, Unsubscribe, - Observer + Observer, + ExtendState } from 'redux' import 'symbol-observable' @@ -19,6 +20,41 @@ type State = { } } +/* extended state */ +const noExtend: ExtendState = { + a: 'a', + b: { + c: 'c', + d: 'd' + } +} +// typings:expect-error +const noExtendError: ExtendState = { + a: 'a', + b: { + c: 'c', + d: 'd' + }, + e: 'oops' +} + +const yesExtend: ExtendState = { + a: 'a', + b: { + c: 'c', + d: 'd' + }, + yes: 'we can' +} +// typings:expect-error +const yesExtendError: ExtendState = { + a: 'a', + b: { + c: 'c', + d: 'd' + } +} + interface DerivedAction extends Action { type: 'a' b: 'b' From 4270e0e12c8354cf539ab2030a01695d3c1b3b99 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Tue, 3 Sep 2019 14:36:59 -0400 Subject: [PATCH 20/21] fix typing issues not caught before --- src/applyMiddleware.ts | 2 +- src/createStore.ts | 2 +- src/types/store.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/applyMiddleware.ts b/src/applyMiddleware.ts index fe8edefa11..7bddf3fc05 100644 --- a/src/applyMiddleware.ts +++ b/src/applyMiddleware.ts @@ -51,7 +51,7 @@ export default function applyMiddleware( ): StoreEnhancer<{ dispatch: Ext }> export default function applyMiddleware( ...middlewares: Middleware[] -): StoreEnhancer { +): StoreEnhancer { return (createStore: StoreCreator) => ( reducer: Reducer, ...args: any[] diff --git a/src/createStore.ts b/src/createStore.ts index b6c2439414..0a0d35b835 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -55,7 +55,7 @@ export default function createStore< >( reducer: Reducer, preloadedState?: PreloadedState, - enhancer?: StoreEnhancer + enhancer?: StoreEnhancer ): Store, A, StateExt, Ext> & Ext export default function createStore< S, diff --git a/src/types/store.ts b/src/types/store.ts index 09a3585dd0..a1fc76e99e 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -254,10 +254,10 @@ 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 = ( +export type StoreEnhancer = ( next: StoreEnhancerStoreCreator ) => StoreEnhancerStoreCreator -export type StoreEnhancerStoreCreator = < +export type StoreEnhancerStoreCreator = < S = any, A extends Action = AnyAction >( From 4ed78cb4ec7810eecee276d52f5941e0d2b1edf6 Mon Sep 17 00:00:00 2001 From: Gregory Beaver Date: Tue, 3 Sep 2019 14:53:20 -0400 Subject: [PATCH 21/21] add link to the place I learned about this --- src/types/store.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/store.ts b/src/types/store.ts index a1fc76e99e..aaebb0ad21 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -8,6 +8,9 @@ import { Reducer } from './reducers' * 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 = [Extension] extends [never] ? State