diff --git a/docs/api/configureStore.mdx b/docs/api/configureStore.mdx index c1fe26beb7..a5b05db21d 100644 --- a/docs/api/configureStore.mdx +++ b/docs/api/configureStore.mdx @@ -40,7 +40,6 @@ Redux Toolkit's `configureStore` simplifies that setup process, by doing all tha interface ConfigureStoreOptions< S = any, - A extends Action = UnknownAction, M extends Tuple> = Tuple> E extends Tuple = Tuple, P = S @@ -49,13 +48,16 @@ interface ConfigureStoreOptions< * A single reducer function that will be used as the root reducer, or an * object of slice reducers that will be passed to `combineReducers()`. */ - reducer: Reducer | ReducersMapObject + reducer: Reducer | ReducersMapObject /** - * An array of Redux middleware to install. If not supplied, defaults to - * the set of middleware returned by `getDefaultMiddleware()`. + * A callback which receives `getDefaultMiddleware` and should return a Tuple + * of middleware to install. If not supplied, defaults to the set of + * middleware returned by `getDefaultMiddleware()`. + * + * @example `middleware: (gDM) => gDM().concat(logger, apiMiddleware, yourCustomMiddleware)` */ - middleware?: ((getDefaultMiddleware: CurriedGetDefaultMiddleware) => M) | M + middleware?: (getDefaultMiddleware: GetDefaultMiddleware) => M /** * Whether to enable Redux DevTools integration. Defaults to `true`. @@ -87,16 +89,15 @@ interface ConfigureStoreOptions< * and should return a new array (such as `getDefaultEnhancers().concat(offline)`). * If you only need to add middleware, you can use the `middleware` parameter instead. */ - enhancers?: (getDefaultEnhancers: GetDefaultEnhancers) => E | E + enhancers?: (getDefaultEnhancers: GetDefaultEnhancers) => E } function configureStore< S = any, - A extends Action = UnknownAction, M extends Tuple> = Tuple> E extends Tuple = Tuple, P = S ->(options: ConfigureStoreOptions): EnhancedStore +>(options: ConfigureStoreOptions): EnhancedStore ``` ### `reducer` diff --git a/packages/toolkit/src/configureStore.ts b/packages/toolkit/src/configureStore.ts index ad14e3543f..926b28d6ba 100644 --- a/packages/toolkit/src/configureStore.ts +++ b/packages/toolkit/src/configureStore.ts @@ -2,7 +2,6 @@ import type { Reducer, ReducersMapObject, Middleware, - Action, StoreEnhancer, Store, UnknownAction, @@ -39,7 +38,6 @@ import { buildGetDefaultEnhancers } from './getDefaultEnhancers' */ export interface ConfigureStoreOptions< S = any, - A extends Action = UnknownAction, M extends Tuple> = Tuple>, E extends Tuple = Tuple, P = S, @@ -47,8 +45,13 @@ export interface ConfigureStoreOptions< /** * A single reducer function that will be used as the root reducer, or an * object of slice reducers that will be passed to `combineReducers()`. + * + * The reducer must handle `UnknownAction`, matching the store's dispatch + * type. A root reducer is always called with actions it doesn't recognise + * (`@@INIT`, replaced-reducer actions, thunk-dispatched actions, ...), so + * its `action` parameter cannot be narrowed to a specific action type. */ - reducer: Reducer | ReducersMapObject + reducer: Reducer | ReducersMapObject /** * An array of Redux middleware to install, or a callback receiving `getDefaultMiddleware` and returning a Tuple of middleware. @@ -105,10 +108,9 @@ type Enhancers = ReadonlyArray */ export type EnhancedStore< S = any, - A extends Action = UnknownAction, E extends Enhancers = Enhancers, > = ExtractStoreExtensions & - Store>> + Store>> /** * A friendly abstraction over the standard Redux `createStore()` function. @@ -120,13 +122,12 @@ export type EnhancedStore< */ export function configureStore< S = any, - A extends Action = UnknownAction, M extends Tuple> = Tuple<[ThunkMiddlewareFor]>, E extends Tuple = Tuple< [StoreEnhancer<{ dispatch: ExtractDispatchExtensions }>, StoreEnhancer] >, P = S, ->(options: ConfigureStoreOptions): EnhancedStore { +>(options: ConfigureStoreOptions): EnhancedStore { const getDefaultMiddleware = buildGetDefaultMiddleware() const { @@ -138,12 +139,16 @@ export function configureStore< enhancers = undefined, } = options || {} - let rootReducer: Reducer + let rootReducer: Reducer if (typeof reducer === 'function') { rootReducer = reducer } else if (isPlainObject(reducer)) { - rootReducer = combineReducers(reducer) as unknown as Reducer + rootReducer = combineReducers(reducer) as unknown as Reducer< + S, + UnknownAction, + P + > } else { throw new Error( '`reducer` is a required argument, and must be a function or an object of functions that can be passed to combineReducers', diff --git a/packages/toolkit/src/tests/configureStore.test-d.ts b/packages/toolkit/src/tests/configureStore.test-d.ts index 8e7fa2ad1a..fabfaa3c43 100644 --- a/packages/toolkit/src/tests/configureStore.test-d.ts +++ b/packages/toolkit/src/tests/configureStore.test-d.ts @@ -56,14 +56,43 @@ describe('type tests', () => { expectTypeOf(store).not.toExtend>() }) - test('configureStore() infers the store action type.', () => { - const reducer: Reducer> = () => 0 + test('configureStore() always types the store action as `UnknownAction`.', () => { + // `configureStore` no longer accepts/infers a store-wide action type - the + // store always dispatches `UnknownAction`, matching the root reducer, which + // is always called with actions it doesn't recognise. + // See https://github.com/reduxjs/redux-toolkit/issues/5317 + const reducer: Reducer = () => 0 const store = configureStore({ reducer }) - expectTypeOf(store).toExtend>>() + expectTypeOf(store).toExtend>() + + expectTypeOf(store).not.toExtend>>() + }) + + test('configureStore() rejects a reducer narrowed to a specific action.', () => { + // A root reducer is always called with `@@INIT`, replaced-reducer actions, + // thunk-dispatched actions, etc. - narrowing its `action` parameter below + // `UnknownAction` is unsound, so the `reducer` option rejects it. + const specificReducer: Reducer> = () => 0 + + // @ts-expect-error the reducer must handle `UnknownAction` + configureStore({ reducer: specificReducer }) + + // slice reducers and `combineReducers()` output handle `UnknownAction`, so + // they remain accepted. + const slice = createSlice({ + name: 'counter', + initialState: 0, + reducers: { + increment: (state, action: PayloadAction) => + state + action.payload, + }, + }) + + configureStore({ reducer: slice.reducer }) - expectTypeOf(store).not.toExtend>>() + configureStore({ reducer: combineReducers({ counter: slice.reducer }) }) }) test('configureStore() accepts Tuple for middleware, but not plain array.', () => { diff --git a/packages/toolkit/src/tests/utils/helpers.tsx b/packages/toolkit/src/tests/utils/helpers.tsx index 402fdda91f..9668520dfb 100644 --- a/packages/toolkit/src/tests/utils/helpers.tsx +++ b/packages/toolkit/src/tests/utils/helpers.tsx @@ -181,10 +181,7 @@ export function setupApiStore< } & { [K in keyof R]: ReturnType }, - UnknownAction, - ReturnType extends EnhancedStore - ? M - : never + ReturnType extends EnhancedStore ? M : never > const initialStore = getStore() as StoreType