Skip to content

Commit

Permalink
Fix type of next parameter in StoreEnhancer type
Browse files Browse the repository at this point in the history
  • Loading branch information
Methuselah96 committed Feb 12, 2023
1 parent 29d5d88 commit 5a084a6
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 105 deletions.
9 changes: 2 additions & 7 deletions src/applyMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import compose from './compose'
import { Middleware, MiddlewareAPI } from './types/middleware'
import { AnyAction } from './types/actions'
import {
StoreEnhancer,
Dispatch,
PreloadedState,
StoreEnhancerStoreCreator
} from './types/store'
import { StoreEnhancer, Dispatch, PreloadedState } from './types/store'
import { Reducer } from './types/reducers'

/**
Expand Down Expand Up @@ -60,7 +55,7 @@ export default function applyMiddleware<Ext, S = any>(
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return (createStore: StoreEnhancerStoreCreator) =>
return createStore =>
<S, A extends AnyAction>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
Expand Down
33 changes: 24 additions & 9 deletions src/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ import { kindOf } from './utils/kindOf'
* `import { legacy_createStore as createStore} from 'redux'`
*
*/
export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
export function createStore<
S,
A extends Action,
Ext extends {} = {},
StateExt extends {} = {}
>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, StateExt> & Ext
Expand Down Expand Up @@ -68,12 +73,22 @@ export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
* `import { legacy_createStore as createStore} from 'redux'`
*
*/
export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
export function createStore<
S,
A extends Action,
Ext extends {} = {},
StateExt extends {} = {}
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, StateExt> & Ext
export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
export function createStore<
S,
A extends Action,
Ext extends {} = {},
StateExt extends {} = {}
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
enhancer?: StoreEnhancer<Ext, StateExt>
Expand Down Expand Up @@ -401,8 +416,8 @@ export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
export function legacy_createStore<
S,
A extends Action,
Ext = {},
StateExt = never
Ext extends {} = {},
StateExt extends {} = {}
>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
Expand Down Expand Up @@ -440,8 +455,8 @@ export function legacy_createStore<
export function legacy_createStore<
S,
A extends Action,
Ext = {},
StateExt = never
Ext extends {} = {},
StateExt extends {} = {}
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
Expand All @@ -450,8 +465,8 @@ export function legacy_createStore<
export function legacy_createStore<
S,
A extends Action,
Ext = {},
StateExt = never
Ext extends {} = {},
StateExt extends {} = {}
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
Expand Down
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ export {
Store,
StoreCreator,
StoreEnhancer,
StoreEnhancerStoreCreator,
ExtendState
StoreEnhancerStoreCreator
} from './types/store'
// reducers
export {
Expand Down
43 changes: 14 additions & 29 deletions src/types/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,6 @@ import { Action, AnyAction } from './actions'
import { Reducer } from './reducers'
import '../utils/symbol-observable'

/**
* 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 @@ -134,11 +120,7 @@ export type Observer<T> = {
* @template A the type of actions which may be dispatched by this store.
* @template StateExt any extension to state from store enhancers
*/
export interface Store<
S = any,
A extends Action = AnyAction,
StateExt = never
> {
export interface Store<S = any, A extends Action = AnyAction, StateExt = {}> {
/**
* Dispatches an action. It is the only way to trigger a state change.
*
Expand Down Expand Up @@ -172,7 +154,7 @@ export interface Store<
*
* @returns The current state tree of your application.
*/
getState(): ExtendState<S, StateExt>
getState(): S & StateExt

/**
* Adds a change listener. It will be called any time an action is
Expand Down Expand Up @@ -217,7 +199,7 @@ export interface Store<
* For more information, see the observable proposal:
* https://github.com/tc39/proposal-observable
*/
[Symbol.observable](): Observable<ExtendState<S, StateExt>>
[Symbol.observable](): Observable<S & StateExt>
}

/**
Expand All @@ -232,11 +214,11 @@ export interface Store<
* @template StateExt State extension that is mixed into the state type.
*/
export interface StoreCreator {
<S, A extends Action, Ext = {}, StateExt = never>(
<S, A extends Action, Ext extends {} = {}, StateExt extends {} = {}>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, StateExt> & Ext
<S, A extends Action, Ext = {}, StateExt = never>(
<S, A extends Action, Ext extends {} = {}, StateExt extends {} = {}>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext>
Expand Down Expand Up @@ -264,13 +246,16 @@ 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 = never> = (
next: StoreEnhancerStoreCreator<Ext, StateExt>
) => StoreEnhancerStoreCreator<Ext, StateExt>
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <
S = any,
A extends Action = AnyAction
export type StoreEnhancer<Ext extends {} = {}, StateExt extends {} = {}> = <
NextExt extends {},
NextStateExt extends {}
>(
next: StoreEnhancerStoreCreator<NextExt, NextStateExt>
) => StoreEnhancerStoreCreator<NextExt & Ext, NextStateExt & StateExt>
export type StoreEnhancerStoreCreator<
Ext extends {} = {},
StateExt extends {} = {}
> = <S = any, A extends Action = AnyAction>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
) => Store<S, A, StateExt> & Ext
92 changes: 76 additions & 16 deletions test/typescript/enhancers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ function stateExtension() {
reducer: Reducer<S, A>,
preloadedState?: any
) => {
const wrappedReducer: Reducer<S & ExtraState, A> = (state, action) => {
const newState = reducer(state, action)
return {
...newState,
extraField: 'extra'
function wrapReducer(reducer: Reducer<S, A>): Reducer<S & ExtraState, A> {
return (state, action) => {
const newState = reducer(state, action)
return {
...newState,
extraField: 'extra'
}
}
}
const wrappedPreloadedState = preloadedState
Expand All @@ -77,7 +79,13 @@ function stateExtension() {
extraField: 'extra'
}
: undefined
return createStore(wrappedReducer, wrappedPreloadedState)
const store = createStore(wrapReducer(reducer), wrappedPreloadedState)
return {
...store,
replaceReducer(nextReducer: Reducer<S, A>) {
store.replaceReducer(wrapReducer(nextReducer))
}
}
}

const store = createStore(reducer, enhancer)
Expand All @@ -96,8 +104,10 @@ function extraMethods() {
createStore =>
(...args) => {
const store = createStore(...args)
store.method = () => 'foo'
return store
return {
...store,
method: () => 'foo'
}
}

const store = createStore(reducer, enhancer)
Expand All @@ -122,11 +132,13 @@ function replaceReducerExtender() {
reducer: Reducer<S, A>,
preloadedState?: any
) => {
const wrappedReducer: Reducer<S & ExtraState, A> = (state, action) => {
const newState = reducer(state, action)
return {
...newState,
extraField: 'extra'
function wrapReducer(reducer: Reducer<S, A>): Reducer<S & ExtraState, A> {
return (state, action) => {
const newState = reducer(state, action)
return {
...newState,
extraField: 'extra'
}
}
}
const wrappedPreloadedState = preloadedState
Expand All @@ -135,7 +147,14 @@ function replaceReducerExtender() {
extraField: 'extra'
}
: undefined
return createStore(wrappedReducer, wrappedPreloadedState)
const store = createStore(wrapReducer(reducer), wrappedPreloadedState)
return {
...store,
replaceReducer(nextReducer: Reducer<S, A>) {
store.replaceReducer(wrapReducer(nextReducer))
},
method: () => 'foo'
}
}

const store = createStore<
Expand Down Expand Up @@ -270,14 +289,14 @@ function finalHelmersonExample() {
<S, A extends Action<unknown>>(
reducer: Reducer<S, A>,
preloadedState?: any
): Store<S, A, ExtraState> & { persistor: Store<S, A, ExtraState> } => {
) => {
const persistedReducer = persistReducer<S, A>(persistConfig, reducer)
const store = createStore(persistedReducer, preloadedState)
const persistor = persistStore(store)

return {
...store,
replaceReducer: nextReducer => {
replaceReducer: (nextReducer: Reducer<S, A>) => {
store.replaceReducer(persistReducer(persistConfig, nextReducer))
},
persistor
Expand Down Expand Up @@ -308,3 +327,44 @@ function finalHelmersonExample() {
// @ts-expect-error
store.getState().wrongField
}

function composedEnhancers() {
interface State {
someState: string
}
const reducer: Reducer<State> = null as any

interface Ext1 {
enhancer1: string
}
interface Ext2 {
enhancer2: number
}

const enhancer1: StoreEnhancer<Ext1> =
createStore => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
return {
...store,
enhancer1: 'foo'
}
}

const enhancer2: StoreEnhancer<Ext2> =
createStore => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
return {
...store,
enhancer2: 5
}
}

const composedEnhancer: StoreEnhancer<Ext1 & Ext2> = createStore =>
enhancer2(enhancer1(createStore))

const enhancedStore = createStore(reducer, composedEnhancer)
enhancedStore.enhancer1
enhancedStore.enhancer2
// @ts-expect-error
enhancedStore.enhancer3
}
43 changes: 1 addition & 42 deletions test/typescript/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {
Action,
StoreEnhancer,
Unsubscribe,
Observer,
ExtendState
Observer
} from '../../src'
import 'symbol-observable'

Expand All @@ -22,46 +21,6 @@ type State = {
e: BrandedString
}

/* extended state */
const noExtend: ExtendState<State, never> = {
a: 'a',
b: {
c: 'c',
d: 'd'
},
e: brandedString
}

const noExtendError: ExtendState<State, never> = {
a: 'a',
b: {
c: 'c',
d: 'd'
},
e: brandedString,
// @ts-expect-error
f: 'oops'
}

const yesExtend: ExtendState<State, { yes: 'we can' }> = {
a: 'a',
b: {
c: 'c',
d: 'd'
},
e: brandedString,
yes: 'we can'
}
// @ts-expect-error
const yesExtendError: ExtendState<State, { yes: 'we can' }> = {
a: 'a',
b: {
c: 'c',
d: 'd'
},
e: brandedString
}

interface DerivedAction extends Action {
type: 'a'
b: 'b'
Expand Down

0 comments on commit 5a084a6

Please sign in to comment.