| 1 | import 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'
|
|---|
| 15 | import {
|
|---|
| 16 | Tuple,
|
|---|
| 17 | applyMiddleware,
|
|---|
| 18 | combineReducers,
|
|---|
| 19 | configureStore,
|
|---|
| 20 | createSlice,
|
|---|
| 21 | } from '@reduxjs/toolkit'
|
|---|
| 22 | import { thunk } from 'redux-thunk'
|
|---|
| 23 |
|
|---|
| 24 | const _anyMiddleware: any = () => () => () => {}
|
|---|
| 25 |
|
|---|
| 26 | describe('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 | })
|
|---|