Skip to content

Update types with PreloadedState generic #3198

New issue

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

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

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion packages/toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
],
"dependencies": {
"immer": "^10.0.0-beta.4",
"redux": "5.0.0-alpha.4",
"redux": "5.0.0-alpha.5",
"redux-thunk": "3.0.0-alpha.3",
"reselect": "^4.1.7"
},
Expand Down
27 changes: 17 additions & 10 deletions packages/toolkit/src/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import type {
StoreEnhancer,
Store,
Dispatch,
PreloadedState,
CombinedState,
} from 'redux'
import { createStore, compose, applyMiddleware, combineReducers } from 'redux'
import type { DevToolsEnhancerOptions as DevToolsOptions } from './devtoolsExtension'
Expand All @@ -21,7 +19,6 @@ import type {
} from './getDefaultMiddleware'
import { curryGetDefaultMiddleware } from './getDefaultMiddleware'
import type {
NoInfer,
ExtractDispatchExtensions,
ExtractStoreExtensions,
} from './tsHelpers'
Expand All @@ -46,13 +43,16 @@ export interface ConfigureStoreOptions<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = Middlewares<S>,
E extends Enhancers = Enhancers
E extends Enhancers = Enhancers,
PreloadedState = S
> {
/**
* 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<S, A> | ReducersMapObject<S, A>
reducer:
| Reducer<S, A, PreloadedState>
| ReducersMapObject<S, A, PreloadedState>

/**
* An array of Redux middleware to install. If not supplied, defaults to
Expand Down Expand Up @@ -87,7 +87,7 @@ export interface ConfigureStoreOptions<
As we cannot distinguish between those two cases without adding another generic parameter,
we just make the pragmatic assumption that the latter almost never happens.
*/
preloadedState?: PreloadedState<CombinedState<NoInfer<S>>>
preloadedState?: PreloadedState

/**
* The store enhancers to apply. See Redux's `createStore()`.
Expand Down Expand Up @@ -142,8 +142,11 @@ export function configureStore<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = [ThunkMiddlewareFor<S>],
E extends Enhancers = [StoreEnhancer]
>(options: ConfigureStoreOptions<S, A, M, E>): EnhancedStore<S, A, M, E> {
E extends Enhancers = [StoreEnhancer],
PreloadedState = S
>(
options: ConfigureStoreOptions<S, A, M, E, PreloadedState>
): EnhancedStore<S, A, M, E> {
const curriedGetDefaultMiddleware = curryGetDefaultMiddleware<S>()

const {
Expand All @@ -154,12 +157,16 @@ export function configureStore<
enhancers = undefined,
} = options || {}

let rootReducer: Reducer<S, A>
let rootReducer: Reducer<S, A, PreloadedState>

if (typeof reducer === 'function') {
rootReducer = reducer
} else if (isPlainObject(reducer)) {
rootReducer = combineReducers(reducer) as unknown as Reducer<S, A>
rootReducer = combineReducers(reducer) as unknown as Reducer<
S,
A,
PreloadedState
>
} 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'
Expand Down
5 changes: 1 addition & 4 deletions packages/toolkit/src/query/core/buildSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
prepareAutoBatched,
} from '@reduxjs/toolkit'
import type {
CombinedState as CombinedQueryState,
QuerySubstateIdentifier,
QuerySubState,
MutationSubstateIdentifier,
Expand Down Expand Up @@ -469,9 +468,7 @@ export function buildSlice({
},
})

const combinedReducer = combineReducers<
CombinedQueryState<any, string, string>
>({
const combinedReducer = combineReducers({
queries: querySlice.reducer,
mutations: mutationSlice.reducer,
provided: invalidationSlice.reducer,
Expand Down
167 changes: 152 additions & 15 deletions packages/toolkit/src/tests/configureStore.typetest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
Action,
StoreEnhancer,
} from 'redux'
import { applyMiddleware } from 'redux'
import { applyMiddleware, combineReducers } from 'redux'
import type { PayloadAction, ConfigureStoreOptions } from '@reduxjs/toolkit'
import {
configureStore,
Expand Down Expand Up @@ -130,8 +130,8 @@ const _anyMiddleware: any = () => () => () => {}
})

configureStore({
reducer: () => 0,
// @ts-expect-error
reducer: (_: number) => 0,
preloadedState: 'non-matching state type',
})
}
Expand Down Expand Up @@ -197,25 +197,162 @@ const _anyMiddleware: any = () => () => () => {}
}

/**
* Test: configureStore() state type inference works when specifying both a
* reducer object and a partial preloaded state.
* Test: Preloaded state typings
*/
{
let counterReducer1: Reducer<number> = () => 0
let counterReducer2: Reducer<number> = () => 0

const store = configureStore({
reducer: {
counter1: counterReducer1,
counter2: counterReducer2,
},
preloadedState: {
counter1: 0,
},
})
/**
* Test: partial preloaded state
*/
{
const store = configureStore({
reducer: {
counter1: counterReducer1,
counter2: counterReducer2,
},
preloadedState: {
counter1: 0,
},
})

const counter1: number = store.getState().counter1
const counter2: number = store.getState().counter2
}

/**
* Test: empty preloaded state
*/
{
const store = configureStore({
reducer: {
counter1: counterReducer1,
counter2: counterReducer2,
},
preloadedState: {},
})

const counter1: number = store.getState().counter1
const counter2: number = store.getState().counter2
}

/**
* Test: excess properties in preloaded state
*/
{
const store = configureStore({
reducer: {
// @ts-expect-error
counter1: counterReducer1,
counter2: counterReducer2,
},
preloadedState: {
counter1: 0,
counter3: 5,
},
})

const counter1: number = store.getState().counter1
const counter2: number = store.getState().counter2
}

/**
* Test: mismatching properties in preloaded state
*/
{
const store = configureStore({
reducer: {
// @ts-expect-error
counter1: counterReducer1,
counter2: counterReducer2,
},
preloadedState: {
counter3: 5,
},
})

const counter1: number = store.getState().counter1
const counter2: number = store.getState().counter2
}

/**
* Test: string preloaded state when expecting object
*/
{
const store = configureStore({
reducer: {
// @ts-expect-error
counter1: counterReducer1,
counter2: counterReducer2,
},
preloadedState: 'test',
})

const counter1: number = store.getState().counter1
const counter2: number = store.getState().counter2
}

const counter1: number = store.getState().counter1
const counter2: number = store.getState().counter2
/**
* Test: nested combineReducers allows partial
*/
{
const store = configureStore({
reducer: {
group1: combineReducers({
counter1: counterReducer1,
counter2: counterReducer2,
}),
group2: combineReducers({
counter1: counterReducer1,
counter2: counterReducer2,
}),
},
preloadedState: {
group1: {
counter1: 5,
},
},
})

const group1counter1: number = store.getState().group1.counter1
const group1counter2: number = store.getState().group1.counter2
const group2counter1: number = store.getState().group2.counter1
const group2counter2: number = store.getState().group2.counter2
}

/**
* Test: non-nested combineReducers does not allow partial
*/
{
interface GroupState {
counter1: number
counter2: number
}

const initialState = { counter1: 0, counter2: 0 }

const group1Reducer: Reducer<GroupState> = (state = initialState) => state
const group2Reducer: Reducer<GroupState> = (state = initialState) => state

const store = configureStore({
reducer: {
// @ts-expect-error
group1: group1Reducer,
group2: group2Reducer,
},
preloadedState: {
group1: {
counter1: 5,
},
},
})

const group1counter1: number = store.getState().group1.counter1
const group1counter2: number = store.getState().group1.counter2
const group2counter1: number = store.getState().group2.counter1
const group2counter2: number = store.getState().group2.counter2
}
}

/**
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6781,7 +6781,7 @@ __metadata:
node-fetch: ^2.6.1
prettier: ^2.2.1
query-string: ^7.0.1
redux: 5.0.0-alpha.4
redux: 5.0.0-alpha.5
redux-thunk: 3.0.0-alpha.3
reselect: ^4.1.7
rimraf: ^3.0.2
Expand Down Expand Up @@ -24576,10 +24576,10 @@ fsevents@^1.2.7:
languageName: node
linkType: hard

"redux@npm:5.0.0-alpha.4":
version: 5.0.0-alpha.4
resolution: "redux@npm:5.0.0-alpha.4"
checksum: ebc98a74d84341df6db87222b6e54a658d68233924315ff67b4b2988ca0ea359e39632f7a43b65ec361ea7ba5714de4e34d76cb0b20089e785d11dc9e5a9e85e
"redux@npm:5.0.0-alpha.5":
version: 5.0.0-alpha.5
resolution: "redux@npm:5.0.0-alpha.5"
checksum: 4223be43f605c0d514d5d611a281ae703f905ed4c6014c81b55d1f59cdeac38e3c82fcee2671b102f6b681d95c2a6a9a0f598e044916378011fa0aa39dc644ad
languageName: node
linkType: hard

Expand Down