Skip to content

Fix type of next parameter in StoreEnhancer type #3776

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 1 commit into from
Feb 12, 2023
Merged
Changes from all commits
Commits
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
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'

/**
@@ -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>
33 changes: 24 additions & 9 deletions src/createStore.ts
Original file line number Diff line number Diff line change
@@ -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
@@ -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>
@@ -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>
@@ -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>,
@@ -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>,
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -18,8 +18,7 @@ export {
Store,
StoreCreator,
StoreEnhancer,
StoreEnhancerStoreCreator,
ExtendState
StoreEnhancerStoreCreator
} from './types/store'
// reducers
export {
43 changes: 14 additions & 29 deletions src/types/store.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
@@ -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.
*
@@ -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
@@ -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>
}

/**
@@ -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>
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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
@@ -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<
@@ -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
@@ -308,3 +327,44 @@ function finalHelmersonExample() {
// @ts-expect-error
store.getState().wrongField
}

function composedEnhancers() {
Copy link
Member Author

@Methuselah96 Methuselah96 May 13, 2020

Choose a reason for hiding this comment

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

This is a new test that would fail without this fix.

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
@@ -5,8 +5,7 @@ import {
Action,
StoreEnhancer,
Unsubscribe,
Observer,
ExtendState
Observer
} from '../../src'
import 'symbol-observable'

@@ -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'