source: node_modules/@reduxjs/toolkit/src/tests/configureStore.test-d.ts@ a762898

Last change on this file since a762898 was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 21.1 KB
Line 
1import type {
2 Action,
3 ConfigureStoreOptions,
4 Dispatch,
5 Middleware,
6 PayloadAction,
7 Reducer,
8 Store,
9 StoreEnhancer,
10 ThunkAction,
11 ThunkDispatch,
12 ThunkMiddleware,
13 UnknownAction,
14} from '@reduxjs/toolkit'
15import {
16 Tuple,
17 applyMiddleware,
18 combineReducers,
19 configureStore,
20 createSlice,
21} from '@reduxjs/toolkit'
22import { thunk } from 'redux-thunk'
23
24const _anyMiddleware: any = () => () => () => {}
25
26describe('type tests', () => {
27 test('configureStore() requires a valid reducer or reducer map.', () => {
28 configureStore({
29 reducer: (state, action) => 0,
30 })
31
32 configureStore({
33 reducer: {
34 counter1: () => 0,
35 counter2: () => 1,
36 },
37 })
38
39 // @ts-expect-error
40 configureStore({ reducer: 'not a reducer' })
41
42 // @ts-expect-error
43 configureStore({ reducer: { a: 'not a reducer' } })
44
45 // @ts-expect-error
46 configureStore({})
47 })
48
49 test('configureStore() infers the store state type.', () => {
50 const reducer: Reducer<number> = () => 0
51
52 const store = configureStore({ reducer })
53
54 expectTypeOf(store).toMatchTypeOf<Store<number, UnknownAction>>()
55
56 expectTypeOf(store).not.toMatchTypeOf<Store<string, UnknownAction>>()
57 })
58
59 test('configureStore() infers the store action type.', () => {
60 const reducer: Reducer<number, PayloadAction<number>> = () => 0
61
62 const store = configureStore({ reducer })
63
64 expectTypeOf(store).toMatchTypeOf<Store<number, PayloadAction<number>>>()
65
66 expectTypeOf(store).not.toMatchTypeOf<
67 Store<number, PayloadAction<string>>
68 >()
69 })
70
71 test('configureStore() accepts Tuple for middleware, but not plain array.', () => {
72 const middleware: Middleware = (store) => (next) => next
73
74 configureStore({
75 reducer: () => 0,
76 middleware: () => new Tuple(middleware),
77 })
78
79 configureStore({
80 reducer: () => 0,
81 // @ts-expect-error
82 middleware: () => [middleware],
83 })
84
85 configureStore({
86 reducer: () => 0,
87 // @ts-expect-error
88 middleware: () => new Tuple('not middleware'),
89 })
90 })
91
92 test('configureStore() accepts devTools flag.', () => {
93 configureStore({
94 reducer: () => 0,
95 devTools: true,
96 })
97
98 configureStore({
99 reducer: () => 0,
100 // @ts-expect-error
101 devTools: 'true',
102 })
103 })
104
105 test('configureStore() accepts devTools EnhancerOptions.', () => {
106 configureStore({
107 reducer: () => 0,
108 devTools: { name: 'myApp' },
109 })
110
111 configureStore({
112 reducer: () => 0,
113 // @ts-expect-error
114 devTools: { appName: 'myApp' },
115 })
116 })
117
118 test('configureStore() accepts preloadedState.', () => {
119 configureStore({
120 reducer: () => 0,
121 preloadedState: 0,
122 })
123
124 configureStore({
125 // @ts-expect-error
126 reducer: (_: number) => 0,
127 preloadedState: 'non-matching state type',
128 })
129 })
130
131 test('nullable state is preserved', () => {
132 const store = configureStore({
133 reducer: (): string | null => null,
134 })
135
136 expectTypeOf(store.getState()).toEqualTypeOf<string | null>()
137 })
138
139 test('configureStore() accepts store Tuple for enhancers, but not plain array', () => {
140 const enhancer = applyMiddleware(() => (next) => next)
141
142 const store = configureStore({
143 reducer: () => 0,
144 enhancers: () => new Tuple(enhancer),
145 })
146
147 const store2 = configureStore({
148 reducer: () => 0,
149 // @ts-expect-error
150 enhancers: () => [enhancer],
151 })
152
153 expectTypeOf(store.dispatch).toMatchTypeOf<
154 Dispatch & ThunkDispatch<number, undefined, UnknownAction>
155 >()
156
157 configureStore({
158 reducer: () => 0,
159 // @ts-expect-error
160 enhancers: () => new Tuple('not a store enhancer'),
161 })
162
163 const somePropertyStoreEnhancer: StoreEnhancer<{
164 someProperty: string
165 }> = (next) => {
166 return (reducer, preloadedState) => {
167 return {
168 ...next(reducer, preloadedState),
169 someProperty: 'some value',
170 }
171 }
172 }
173
174 const anotherPropertyStoreEnhancer: StoreEnhancer<{
175 anotherProperty: number
176 }> = (next) => {
177 return (reducer, preloadedState) => {
178 return {
179 ...next(reducer, preloadedState),
180 anotherProperty: 123,
181 }
182 }
183 }
184
185 const store3 = configureStore({
186 reducer: () => 0,
187 enhancers: () =>
188 new Tuple(somePropertyStoreEnhancer, anotherPropertyStoreEnhancer),
189 })
190
191 expectTypeOf(store3.dispatch).toEqualTypeOf<Dispatch>()
192
193 expectTypeOf(store3.someProperty).toBeString()
194
195 expectTypeOf(store3.anotherProperty).toBeNumber()
196
197 const storeWithCallback = configureStore({
198 reducer: () => 0,
199 enhancers: (getDefaultEnhancers) =>
200 getDefaultEnhancers()
201 .prepend(anotherPropertyStoreEnhancer)
202 .concat(somePropertyStoreEnhancer),
203 })
204
205 expectTypeOf(store3.dispatch).toMatchTypeOf<
206 Dispatch & ThunkDispatch<number, undefined, UnknownAction>
207 >()
208
209 expectTypeOf(store3.someProperty).toBeString()
210
211 expectTypeOf(store3.anotherProperty).toBeNumber()
212
213 const someStateExtendingEnhancer: StoreEnhancer<
214 {},
215 { someProperty: string }
216 > =
217 (next) =>
218 (...args) => {
219 const store = next(...args)
220 const getState = () => ({
221 ...store.getState(),
222 someProperty: 'some value',
223 })
224 return {
225 ...store,
226 getState,
227 } as any
228 }
229
230 const anotherStateExtendingEnhancer: StoreEnhancer<
231 {},
232 { anotherProperty: number }
233 > =
234 (next) =>
235 (...args) => {
236 const store = next(...args)
237 const getState = () => ({
238 ...store.getState(),
239 anotherProperty: 123,
240 })
241 return {
242 ...store,
243 getState,
244 } as any
245 }
246
247 const store4 = configureStore({
248 reducer: () => ({ aProperty: 0 }),
249 enhancers: () =>
250 new Tuple(someStateExtendingEnhancer, anotherStateExtendingEnhancer),
251 })
252
253 const state = store4.getState()
254
255 expectTypeOf(state.aProperty).toBeNumber()
256
257 expectTypeOf(state.someProperty).toBeString()
258
259 expectTypeOf(state.anotherProperty).toBeNumber()
260
261 const storeWithCallback2 = configureStore({
262 reducer: () => ({ aProperty: 0 }),
263 enhancers: (gDE) =>
264 gDE().concat(someStateExtendingEnhancer, anotherStateExtendingEnhancer),
265 })
266
267 const stateWithCallback = storeWithCallback2.getState()
268
269 expectTypeOf(stateWithCallback.aProperty).toBeNumber()
270
271 expectTypeOf(stateWithCallback.someProperty).toBeString()
272
273 expectTypeOf(stateWithCallback.anotherProperty).toBeNumber()
274 })
275
276 test('Preloaded state typings', () => {
277 const counterReducer1: Reducer<number> = () => 0
278 const counterReducer2: Reducer<number> = () => 0
279
280 test('partial preloaded state', () => {
281 const store = configureStore({
282 reducer: {
283 counter1: counterReducer1,
284 counter2: counterReducer2,
285 },
286 preloadedState: {
287 counter1: 0,
288 },
289 })
290
291 expectTypeOf(store.getState().counter1).toBeNumber()
292
293 expectTypeOf(store.getState().counter2).toBeNumber()
294 })
295
296 test('empty preloaded state', () => {
297 const store = configureStore({
298 reducer: {
299 counter1: counterReducer1,
300 counter2: counterReducer2,
301 },
302 preloadedState: {},
303 })
304
305 expectTypeOf(store.getState().counter1).toBeNumber()
306
307 expectTypeOf(store.getState().counter2).toBeNumber()
308 })
309
310 test('excess properties in preloaded state', () => {
311 const store = configureStore({
312 reducer: {
313 // @ts-expect-error
314 counter1: counterReducer1,
315 counter2: counterReducer2,
316 },
317 preloadedState: {
318 counter1: 0,
319 counter3: 5,
320 },
321 })
322
323 expectTypeOf(store.getState().counter1).toBeNumber()
324
325 expectTypeOf(store.getState().counter2).toBeNumber()
326 })
327
328 test('mismatching properties in preloaded state', () => {
329 const store = configureStore({
330 reducer: {
331 // @ts-expect-error
332 counter1: counterReducer1,
333 counter2: counterReducer2,
334 },
335 preloadedState: {
336 counter3: 5,
337 },
338 })
339
340 expectTypeOf(store.getState().counter1).toBeNumber()
341
342 expectTypeOf(store.getState().counter2).toBeNumber()
343 })
344
345 test('string preloaded state when expecting object', () => {
346 const store = configureStore({
347 reducer: {
348 // @ts-expect-error
349 counter1: counterReducer1,
350 counter2: counterReducer2,
351 },
352 preloadedState: 'test',
353 })
354
355 expectTypeOf(store.getState().counter1).toBeNumber()
356
357 expectTypeOf(store.getState().counter2).toBeNumber()
358 })
359
360 test('nested combineReducers allows partial', () => {
361 const store = configureStore({
362 reducer: {
363 group1: combineReducers({
364 counter1: counterReducer1,
365 counter2: counterReducer2,
366 }),
367 group2: combineReducers({
368 counter1: counterReducer1,
369 counter2: counterReducer2,
370 }),
371 },
372 preloadedState: {
373 group1: {
374 counter1: 5,
375 },
376 },
377 })
378
379 expectTypeOf(store.getState().group1.counter1).toBeNumber()
380
381 expectTypeOf(store.getState().group1.counter2).toBeNumber()
382
383 expectTypeOf(store.getState().group2.counter1).toBeNumber()
384
385 expectTypeOf(store.getState().group2.counter2).toBeNumber()
386 })
387
388 test('non-nested combineReducers does not allow partial', () => {
389 interface GroupState {
390 counter1: number
391 counter2: number
392 }
393
394 const initialState = { counter1: 0, counter2: 0 }
395
396 const group1Reducer: Reducer<GroupState> = (state = initialState) => state
397 const group2Reducer: Reducer<GroupState> = (state = initialState) => state
398
399 const store = configureStore({
400 reducer: {
401 // @ts-expect-error
402 group1: group1Reducer,
403 group2: group2Reducer,
404 },
405 preloadedState: {
406 group1: {
407 counter1: 5,
408 },
409 },
410 })
411
412 expectTypeOf(store.getState().group1.counter1).toBeNumber()
413
414 expectTypeOf(store.getState().group1.counter2).toBeNumber()
415
416 expectTypeOf(store.getState().group2.counter1).toBeNumber()
417
418 expectTypeOf(store.getState().group2.counter2).toBeNumber()
419 })
420 })
421
422 test('Dispatch typings', () => {
423 type StateA = number
424 const reducerA = () => 0
425 const thunkA = () => {
426 return (() => {}) as any as ThunkAction<Promise<'A'>, StateA, any, any>
427 }
428
429 type StateB = string
430 const thunkB = () => {
431 return (dispatch: Dispatch, getState: () => StateB) => {}
432 }
433
434 test('by default, dispatching Thunks is possible', () => {
435 const store = configureStore({
436 reducer: reducerA,
437 })
438
439 store.dispatch(thunkA())
440 // @ts-expect-error
441 store.dispatch(thunkB())
442
443 const res = store.dispatch((dispatch, getState) => {
444 return 42
445 })
446
447 const action = store.dispatch({ type: 'foo' })
448 })
449
450 test('return type of thunks and actions is inferred correctly', () => {
451 const slice = createSlice({
452 name: 'counter',
453 initialState: {
454 value: 0,
455 },
456 reducers: {
457 incrementByAmount: (state, action: PayloadAction<number>) => {
458 state.value += action.payload
459 },
460 },
461 })
462
463 const store = configureStore({
464 reducer: {
465 counter: slice.reducer,
466 },
467 })
468
469 const action = slice.actions.incrementByAmount(2)
470
471 const dispatchResult = store.dispatch(action)
472
473 expectTypeOf(dispatchResult).toMatchTypeOf<{
474 type: string
475 payload: number
476 }>()
477
478 const promiseResult = store.dispatch(async (dispatch) => {
479 return 42
480 })
481
482 expectTypeOf(promiseResult).toEqualTypeOf<Promise<number>>()
483
484 const store2 = configureStore({
485 reducer: {
486 counter: slice.reducer,
487 },
488 middleware: (gDM) =>
489 gDM({
490 thunk: {
491 extraArgument: 42,
492 },
493 }),
494 })
495
496 const dispatchResult2 = store2.dispatch(action)
497
498 expectTypeOf(dispatchResult2).toMatchTypeOf<{
499 type: string
500 payload: number
501 }>()
502 })
503
504 test('removing the Thunk Middleware', () => {
505 const store = configureStore({
506 reducer: reducerA,
507 middleware: () => new Tuple(),
508 })
509
510 expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkA())
511
512 expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB())
513 })
514
515 test('adding the thunk middleware by hand', () => {
516 const store = configureStore({
517 reducer: reducerA,
518 middleware: () => new Tuple(thunk as ThunkMiddleware<StateA>),
519 })
520
521 store.dispatch(thunkA())
522 // @ts-expect-error
523 store.dispatch(thunkB())
524 })
525
526 test('custom middleware', () => {
527 const store = configureStore({
528 reducer: reducerA,
529 middleware: () =>
530 new Tuple(0 as unknown as Middleware<(a: StateA) => boolean, StateA>),
531 })
532
533 expectTypeOf(store.dispatch(5)).toBeBoolean()
534
535 expectTypeOf(store.dispatch(5)).not.toBeString()
536 })
537
538 test('multiple custom middleware', () => {
539 const middleware = [] as any as Tuple<
540 [
541 Middleware<(a: 'a') => 'A', StateA>,
542 Middleware<(b: 'b') => 'B', StateA>,
543 ThunkMiddleware<StateA>,
544 ]
545 >
546
547 const store = configureStore({
548 reducer: reducerA,
549 middleware: () => middleware,
550 })
551
552 expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>()
553
554 expectTypeOf(store.dispatch('b')).toEqualTypeOf<'B'>()
555
556 expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>()
557 })
558
559 test('Accepts thunk with `unknown`, `undefined` or `null` ThunkAction extraArgument per default', () => {
560 const store = configureStore({ reducer: {} })
561 // undefined is the default value for the ThunkMiddleware extraArgument
562 store.dispatch(function () {} as ThunkAction<
563 void,
564 {},
565 undefined,
566 UnknownAction
567 >)
568 // `null` for the `extra` generic was previously documented in the RTK "Advanced Tutorial", but
569 // is a bad pattern and users should use `unknown` instead
570 // @ts-expect-error
571 store.dispatch(function () {} as ThunkAction<
572 void,
573 {},
574 null,
575 UnknownAction
576 >)
577 // unknown is the best way to type a ThunkAction if you do not care
578 // about the value of the extraArgument, as it will always work with every
579 // ThunkMiddleware, no matter the actual extraArgument type
580 store.dispatch(function () {} as ThunkAction<
581 void,
582 {},
583 unknown,
584 UnknownAction
585 >)
586 // @ts-expect-error
587 store.dispatch(function () {} as ThunkAction<
588 void,
589 {},
590 boolean,
591 UnknownAction
592 >)
593 })
594
595 test('custom middleware and getDefaultMiddleware', () => {
596 const store = configureStore({
597 reducer: reducerA,
598 middleware: (gDM) =>
599 gDM().prepend((() => {}) as any as Middleware<
600 (a: 'a') => 'A',
601 StateA
602 >),
603 })
604
605 expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>()
606
607 expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>()
608
609 expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB())
610 })
611
612 test('custom middleware and getDefaultMiddleware, using prepend', () => {
613 const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> =
614 _anyMiddleware
615
616 const store = configureStore({
617 reducer: reducerA,
618 middleware: (gDM) => {
619 const concatenated = gDM().prepend(otherMiddleware)
620
621 expectTypeOf(concatenated).toMatchTypeOf<
622 ReadonlyArray<
623 typeof otherMiddleware | ThunkMiddleware | Middleware<{}>
624 >
625 >()
626
627 return concatenated
628 },
629 })
630
631 expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>()
632
633 expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>()
634
635 expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB())
636 })
637
638 test('custom middleware and getDefaultMiddleware, using concat', () => {
639 const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> =
640 _anyMiddleware
641
642 const store = configureStore({
643 reducer: reducerA,
644 middleware: (gDM) => {
645 const concatenated = gDM().concat(otherMiddleware)
646
647 expectTypeOf(concatenated).toMatchTypeOf<
648 ReadonlyArray<
649 typeof otherMiddleware | ThunkMiddleware | Middleware<{}>
650 >
651 >()
652
653 return concatenated
654 },
655 })
656
657 expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>()
658
659 expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>()
660
661 expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB())
662 })
663
664 test('middlewareBuilder notation, getDefaultMiddleware (unconfigured)', () => {
665 const store = configureStore({
666 reducer: reducerA,
667 middleware: (getDefaultMiddleware) =>
668 getDefaultMiddleware().prepend((() => {}) as any as Middleware<
669 (a: 'a') => 'A',
670 StateA
671 >),
672 })
673
674 expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>()
675
676 expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>()
677
678 expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB())
679 })
680
681 test('middlewareBuilder notation, getDefaultMiddleware, concat & prepend', () => {
682 const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> =
683 _anyMiddleware
684
685 const otherMiddleware2: Middleware<(a: 'b') => 'B', StateA> =
686 _anyMiddleware
687
688 const store = configureStore({
689 reducer: reducerA,
690 middleware: (getDefaultMiddleware) =>
691 getDefaultMiddleware()
692 .concat(otherMiddleware)
693 .prepend(otherMiddleware2),
694 })
695
696 expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>()
697
698 expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>()
699
700 expectTypeOf(store.dispatch('b')).toEqualTypeOf<'B'>()
701
702 expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB())
703 })
704
705 test('middlewareBuilder notation, getDefaultMiddleware (thunk: false)', () => {
706 const store = configureStore({
707 reducer: reducerA,
708 middleware: (getDefaultMiddleware) =>
709 getDefaultMiddleware({ thunk: false }).prepend(
710 (() => {}) as any as Middleware<(a: 'a') => 'A', StateA>,
711 ),
712 })
713
714 expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>()
715
716 expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkA())
717 })
718
719 test("badly typed middleware won't make `dispatch` `any`", () => {
720 const store = configureStore({
721 reducer: reducerA,
722 middleware: (getDefaultMiddleware) =>
723 getDefaultMiddleware().concat(_anyMiddleware as Middleware<any>),
724 })
725
726 expectTypeOf(store.dispatch).not.toBeAny()
727 })
728
729 test("decorated `configureStore` won't make `dispatch` `never`", () => {
730 const someSlice = createSlice({
731 name: 'something',
732 initialState: null as any,
733 reducers: {
734 set(state) {
735 return state
736 },
737 },
738 })
739
740 function configureMyStore<S>(
741 options: Omit<ConfigureStoreOptions<S>, 'reducer'>,
742 ) {
743 return configureStore({
744 ...options,
745 reducer: someSlice.reducer,
746 })
747 }
748
749 const store = configureMyStore({})
750
751 expectTypeOf(store.dispatch).toBeFunction()
752 })
753
754 interface CounterState {
755 value: number
756 }
757
758 const counterSlice = createSlice({
759 name: 'counter',
760 initialState: { value: 0 } as CounterState,
761 reducers: {
762 increment(state) {
763 state.value += 1
764 },
765 decrement(state) {
766 state.value -= 1
767 },
768 // Use the PayloadAction type to declare the contents of `action.payload`
769 incrementByAmount: (state, action: PayloadAction<number>) => {
770 state.value += action.payload
771 },
772 },
773 })
774
775 type Unsubscribe = () => void
776
777 // A fake middleware that tells TS that an unsubscribe callback is being returned for a given action
778 // This is the same signature that the "listener" middleware uses
779 const dummyMiddleware: Middleware<
780 {
781 (action: Action<'actionListenerMiddleware/add'>): Unsubscribe
782 },
783 CounterState
784 > = (storeApi) => (next) => (action) => {}
785
786 const store = configureStore({
787 reducer: counterSlice.reducer,
788 middleware: (gDM) => gDM().prepend(dummyMiddleware),
789 })
790
791 // Order matters here! We need the listener type to come first, otherwise
792 // the thunk middleware type kicks in and TS thinks a plain action is being returned
793 expectTypeOf(store.dispatch).toEqualTypeOf<
794 ((action: Action<'actionListenerMiddleware/add'>) => Unsubscribe) &
795 ThunkDispatch<CounterState, undefined, UnknownAction> &
796 Dispatch<UnknownAction>
797 >()
798
799 const unsubscribe = store.dispatch({
800 type: 'actionListenerMiddleware/add',
801 } as const)
802
803 expectTypeOf(unsubscribe).toEqualTypeOf<Unsubscribe>()
804 })
805})
Note: See TracBrowser for help on using the repository browser.