Skip to content

Commit 325baf5

Browse files
committed
Infer preloadedstate properly from provided slice
1 parent 78781fd commit 325baf5

File tree

3 files changed

+132
-23
lines changed

3 files changed

+132
-23
lines changed

packages/toolkit/src/combineSlices.ts

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { Reducer, StateFromReducersMapObject, UnknownAction } from 'redux'
1+
import type {
2+
PreloadedStateShapeFromReducersMapObject,
3+
Reducer,
4+
StateFromReducersMapObject,
5+
UnknownAction,
6+
} from 'redux'
27
import { combineReducers } from 'redux'
38
import { nanoid } from './nanoid'
49
import type {
@@ -10,9 +15,9 @@ import type {
1015
} from './tsHelpers'
1116
import { getOrInsertComputed } from './utils'
1217

13-
type SliceLike<ReducerPath extends string, State> = {
18+
type SliceLike<ReducerPath extends string, State, PreloadedState = State> = {
1419
reducerPath: ReducerPath
15-
reducer: Reducer<State>
20+
reducer: Reducer<State, any, PreloadedState>
1621
}
1722

1823
type AnySliceLike = SliceLike<string, any>
@@ -21,18 +26,26 @@ type SliceLikeReducerPath<A extends AnySliceLike> =
2126
A extends SliceLike<infer ReducerPath, any> ? ReducerPath : never
2227

2328
type SliceLikeState<A extends AnySliceLike> =
24-
A extends SliceLike<any, infer State> ? State : never
29+
A extends SliceLike<any, infer State, any> ? State : never
30+
31+
type SliceLikePreloadedState<A extends AnySliceLike> =
32+
A extends SliceLike<any, any, infer PreloadedState> ? PreloadedState : never
2533

2634
export type WithSlice<A extends AnySliceLike> = {
2735
[Path in SliceLikeReducerPath<A>]: SliceLikeState<A>
2836
}
2937

38+
export type WithSlicePreloadedState<A extends AnySliceLike> = {
39+
[Path in SliceLikeReducerPath<A>]: SliceLikePreloadedState<A>
40+
}
41+
3042
type ReducerMap = Record<string, Reducer>
3143

32-
type ExistingSliceLike<DeclaredState> = {
44+
type ExistingSliceLike<DeclaredState, PreloadedState> = {
3345
[ReducerPath in keyof DeclaredState]: SliceLike<
3446
ReducerPath & string,
35-
NonUndefined<DeclaredState[ReducerPath]>
47+
NonUndefined<DeclaredState[ReducerPath]>,
48+
NonUndefined<PreloadedState[ReducerPath & keyof PreloadedState]>
3649
>
3750
}[keyof DeclaredState]
3851

@@ -48,8 +61,11 @@ export type InjectConfig = {
4861
*/
4962
export interface CombinedSliceReducer<
5063
InitialState,
51-
DeclaredState = InitialState,
52-
> extends Reducer<DeclaredState, UnknownAction, Partial<DeclaredState>> {
64+
DeclaredState extends InitialState = InitialState,
65+
PreloadedState extends Partial<
66+
Record<keyof PreloadedState, any>
67+
> = Partial<DeclaredState>,
68+
> extends Reducer<DeclaredState, UnknownAction, PreloadedState> {
5369
/**
5470
* Provide a type for slices that will be injected lazily.
5571
*
@@ -79,9 +95,10 @@ export interface CombinedSliceReducer<
7995
* const withCustom = rootReducer.inject({ reducerPath: "customName", reducer: customSlice.reducer })
8096
* ```
8197
*/
82-
withLazyLoadedSlices<Lazy = {}>(): CombinedSliceReducer<
98+
withLazyLoadedSlices<Lazy = {}, LazyPreloaded = Lazy>(): CombinedSliceReducer<
8399
InitialState,
84-
Id<DeclaredState & Partial<Lazy>>
100+
Id<DeclaredState & Partial<Lazy>>,
101+
Id<PreloadedState & Partial<LazyPreloaded>>
85102
>
86103

87104
/**
@@ -96,10 +113,14 @@ export interface CombinedSliceReducer<
96113
* ```
97114
*
98115
*/
99-
inject<Sl extends Id<ExistingSliceLike<DeclaredState>>>(
116+
inject<Sl extends Id<ExistingSliceLike<DeclaredState, PreloadedState>>>(
100117
slice: Sl,
101118
config?: InjectConfig,
102-
): CombinedSliceReducer<InitialState, Id<DeclaredState & WithSlice<Sl>>>
119+
): CombinedSliceReducer<
120+
InitialState,
121+
Id<DeclaredState & WithSlice<Sl>>,
122+
Id<PreloadedState & Partial<WithSlicePreloadedState<Sl>>>
123+
>
103124

104125
/**
105126
* Inject a slice.
@@ -113,15 +134,21 @@ export interface CombinedSliceReducer<
113134
* ```
114135
*
115136
*/
116-
inject<ReducerPath extends string, State>(
137+
inject<ReducerPath extends string, State, PreloadedState = State>(
117138
slice: SliceLike<
118139
ReducerPath,
119-
State & (ReducerPath extends keyof DeclaredState ? never : State)
140+
State & (ReducerPath extends keyof DeclaredState ? never : State),
141+
PreloadedState &
142+
(ReducerPath extends keyof PreloadedState ? never : PreloadedState)
120143
>,
121144
config?: InjectConfig,
122145
): CombinedSliceReducer<
123146
InitialState,
124-
Id<DeclaredState & WithSlice<SliceLike<ReducerPath, State>>>
147+
Id<DeclaredState & WithSlice<SliceLike<ReducerPath, State>>>,
148+
Id<
149+
PreloadedState &
150+
WithSlicePreloadedState<SliceLike<ReducerPath, State, PreloadedState>>
151+
>
125152
>
126153

127154
/**
@@ -301,14 +328,23 @@ type InitialState<Slices extends Array<AnySliceLike | ReducerMap>> =
301328
: never
302329
>
303330

331+
type InitialPreloadedState<Slices extends Array<AnySliceLike | ReducerMap>> =
332+
UnionToIntersection<
333+
Slices[number] extends infer Slice
334+
? Slice extends AnySliceLike
335+
? WithSlicePreloadedState<Slice>
336+
: PreloadedStateShapeFromReducersMapObject<Slice>
337+
: never
338+
>
339+
304340
const isSliceLike = (
305341
maybeSliceLike: AnySliceLike | ReducerMap,
306342
): maybeSliceLike is AnySliceLike =>
307343
'reducerPath' in maybeSliceLike &&
308344
typeof maybeSliceLike.reducerPath === 'string'
309345

310346
const getReducers = (slices: Array<AnySliceLike | ReducerMap>) =>
311-
slices.flatMap((sliceOrMap) =>
347+
slices.flatMap<[string, Reducer]>((sliceOrMap) =>
312348
isSliceLike(sliceOrMap)
313349
? [[sliceOrMap.reducerPath, sliceOrMap.reducer] as const]
314350
: Object.entries(sliceOrMap),
@@ -370,7 +406,11 @@ const noopReducer: Reducer<Record<string, any>> = (state = emptyObject) => state
370406

371407
export function combineSlices<Slices extends Array<AnySliceLike | ReducerMap>>(
372408
...slices: Slices
373-
): CombinedSliceReducer<Id<InitialState<Slices>>> {
409+
): CombinedSliceReducer<
410+
Id<InitialState<Slices>>,
411+
Id<InitialState<Slices>>,
412+
Partial<Id<InitialPreloadedState<Slices>>>
413+
> {
374414
const reducerMap = Object.fromEntries<Reducer>(getReducers(slices))
375415

376416
const getReducer = () =>

packages/toolkit/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,11 @@ export type { AutoBatchOptions } from './autoBatchEnhancer'
208208

209209
export { combineSlices } from './combineSlices'
210210

211-
export type { CombinedSliceReducer, WithSlice } from './combineSlices'
211+
export type {
212+
CombinedSliceReducer,
213+
WithSlice,
214+
WithSlicePreloadedState,
215+
} from './combineSlices'
212216

213217
export type {
214218
ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions,

packages/toolkit/src/tests/combineSlices.test-d.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { Reducer, Slice, WithSlice } from '@reduxjs/toolkit'
1+
import type {
2+
Action,
3+
Reducer,
4+
Slice,
5+
WithSlice,
6+
WithSlicePreloadedState,
7+
} from '@reduxjs/toolkit'
28
import { combineSlices } from '@reduxjs/toolkit'
39
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
410

@@ -8,6 +14,13 @@ declare const numberSlice: Slice<number, {}, 'number'>
814

915
declare const booleanReducer: Reducer<boolean>
1016

17+
declare const mixedReducer: Reducer<string, Action, number>
18+
19+
declare const mixedSliceLike: {
20+
reducerPath: 'mixedSlice'
21+
reducer: typeof mixedReducer
22+
}
23+
1124
const exampleApi = createApi({
1225
baseQuery: fetchBaseQuery(),
1326
endpoints: (build) => ({
@@ -21,16 +34,31 @@ type ExampleApiState = ReturnType<typeof exampleApi.reducer>
2134

2235
describe('type tests', () => {
2336
test('combineSlices correctly combines static state', () => {
24-
const rootReducer = combineSlices(stringSlice, numberSlice, exampleApi, {
25-
boolean: booleanReducer,
26-
})
37+
const rootReducer = combineSlices(
38+
stringSlice,
39+
numberSlice,
40+
exampleApi,
41+
{
42+
boolean: booleanReducer,
43+
mixed: mixedReducer,
44+
},
45+
mixedSliceLike,
46+
)
2747

2848
expectTypeOf(rootReducer(undefined, { type: '' })).toEqualTypeOf<{
2949
string: string
3050
number: number
3151
boolean: boolean
3252
api: ExampleApiState
53+
mixed: string
54+
mixedSlice: string
3355
}>()
56+
57+
// test for correct preloaded state handling
58+
expectTypeOf(rootReducer).toBeCallableWith(
59+
{ mixed: 9, mixedSlice: 9 },
60+
{ type: '' },
61+
)
3462
})
3563

3664
test('combineSlices allows passing no initial reducers', () => {
@@ -63,7 +91,21 @@ describe('type tests', () => {
6391
test('inject marks injected keys as required', () => {
6492
const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<
6593
WithSlice<typeof numberSlice> &
66-
WithSlice<typeof exampleApi> & { boolean: boolean }
94+
WithSlice<typeof exampleApi> & { boolean: boolean } & WithSlice<
95+
typeof mixedSliceLike
96+
> &
97+
WithSlice<{
98+
reducerPath: 'mixedReducer'
99+
reducer: typeof mixedReducer
100+
}>,
101+
WithSlicePreloadedState<typeof numberSlice> &
102+
WithSlicePreloadedState<typeof exampleApi> & {
103+
boolean: boolean
104+
} & WithSlicePreloadedState<typeof mixedSliceLike> &
105+
WithSlicePreloadedState<{
106+
reducerPath: 'mixedReducer'
107+
reducer: typeof mixedReducer
108+
}>
67109
>()
68110

69111
expectTypeOf(rootReducer(undefined, { type: '' }).number).toEqualTypeOf<
@@ -78,6 +120,14 @@ describe('type tests', () => {
78120
ExampleApiState | undefined
79121
>()
80122

123+
expectTypeOf(rootReducer(undefined, { type: '' }).mixedSlice).toEqualTypeOf<
124+
string | undefined
125+
>()
126+
127+
expectTypeOf(
128+
rootReducer(undefined, { type: '' }).mixedReducer,
129+
).toEqualTypeOf<string | undefined>()
130+
81131
const withNumber = rootReducer.inject(numberSlice)
82132

83133
expectTypeOf(withNumber(undefined, { type: '' }).number).toBeNumber()
@@ -94,6 +144,21 @@ describe('type tests', () => {
94144
expectTypeOf(
95145
withApi(undefined, { type: '' }).api,
96146
).toEqualTypeOf<ExampleApiState>()
147+
148+
const withMixedSlice = rootReducer.inject(mixedSliceLike)
149+
150+
expectTypeOf(
151+
withMixedSlice(undefined, { type: '' }).mixedSlice,
152+
).toBeString()
153+
154+
const withMixedReducer = rootReducer.inject({
155+
reducerPath: 'mixedReducer',
156+
reducer: mixedReducer,
157+
})
158+
159+
expectTypeOf(
160+
withMixedReducer(undefined, { type: '' }).mixedReducer,
161+
).toBeString()
97162
})
98163

99164
test('selector() allows defining selectors with injected reducers defined', () => {

0 commit comments

Comments
 (0)