Spaces:
Running
Running
| import type { | |
| Action, | |
| ConfigureStoreOptions, | |
| Dispatch, | |
| Middleware, | |
| PayloadAction, | |
| Reducer, | |
| Store, | |
| StoreEnhancer, | |
| ThunkAction, | |
| ThunkDispatch, | |
| ThunkMiddleware, | |
| UnknownAction, | |
| } from '@reduxjs/toolkit' | |
| import { | |
| Tuple, | |
| applyMiddleware, | |
| combineReducers, | |
| configureStore, | |
| createSlice, | |
| } from '@reduxjs/toolkit' | |
| import { thunk } from 'redux-thunk' | |
| const _anyMiddleware: any = () => () => () => {} | |
| describe('type tests', () => { | |
| test('configureStore() requires a valid reducer or reducer map.', () => { | |
| configureStore({ | |
| reducer: (state, action) => 0, | |
| }) | |
| configureStore({ | |
| reducer: { | |
| counter1: () => 0, | |
| counter2: () => 1, | |
| }, | |
| }) | |
| // @ts-expect-error | |
| configureStore({ reducer: 'not a reducer' }) | |
| // @ts-expect-error | |
| configureStore({ reducer: { a: 'not a reducer' } }) | |
| // @ts-expect-error | |
| configureStore({}) | |
| }) | |
| test('configureStore() infers the store state type.', () => { | |
| const reducer: Reducer<number> = () => 0 | |
| const store = configureStore({ reducer }) | |
| expectTypeOf(store).toMatchTypeOf<Store<number, UnknownAction>>() | |
| expectTypeOf(store).not.toMatchTypeOf<Store<string, UnknownAction>>() | |
| }) | |
| test('configureStore() infers the store action type.', () => { | |
| const reducer: Reducer<number, PayloadAction<number>> = () => 0 | |
| const store = configureStore({ reducer }) | |
| expectTypeOf(store).toMatchTypeOf<Store<number, PayloadAction<number>>>() | |
| expectTypeOf(store).not.toMatchTypeOf< | |
| Store<number, PayloadAction<string>> | |
| >() | |
| }) | |
| test('configureStore() accepts Tuple for middleware, but not plain array.', () => { | |
| const middleware: Middleware = (store) => (next) => next | |
| configureStore({ | |
| reducer: () => 0, | |
| middleware: () => new Tuple(middleware), | |
| }) | |
| configureStore({ | |
| reducer: () => 0, | |
| // @ts-expect-error | |
| middleware: () => [middleware], | |
| }) | |
| configureStore({ | |
| reducer: () => 0, | |
| // @ts-expect-error | |
| middleware: () => new Tuple('not middleware'), | |
| }) | |
| }) | |
| test('configureStore() accepts devTools flag.', () => { | |
| configureStore({ | |
| reducer: () => 0, | |
| devTools: true, | |
| }) | |
| configureStore({ | |
| reducer: () => 0, | |
| // @ts-expect-error | |
| devTools: 'true', | |
| }) | |
| }) | |
| test('configureStore() accepts devTools EnhancerOptions.', () => { | |
| configureStore({ | |
| reducer: () => 0, | |
| devTools: { name: 'myApp' }, | |
| }) | |
| configureStore({ | |
| reducer: () => 0, | |
| // @ts-expect-error | |
| devTools: { appName: 'myApp' }, | |
| }) | |
| }) | |
| test('configureStore() accepts preloadedState.', () => { | |
| configureStore({ | |
| reducer: () => 0, | |
| preloadedState: 0, | |
| }) | |
| configureStore({ | |
| // @ts-expect-error | |
| reducer: (_: number) => 0, | |
| preloadedState: 'non-matching state type', | |
| }) | |
| }) | |
| test('nullable state is preserved', () => { | |
| const store = configureStore({ | |
| reducer: (): string | null => null, | |
| }) | |
| expectTypeOf(store.getState()).toEqualTypeOf<string | null>() | |
| }) | |
| test('configureStore() accepts store Tuple for enhancers, but not plain array', () => { | |
| const enhancer = applyMiddleware(() => (next) => next) | |
| const store = configureStore({ | |
| reducer: () => 0, | |
| enhancers: () => new Tuple(enhancer), | |
| }) | |
| const store2 = configureStore({ | |
| reducer: () => 0, | |
| // @ts-expect-error | |
| enhancers: () => [enhancer], | |
| }) | |
| expectTypeOf(store.dispatch).toMatchTypeOf< | |
| Dispatch & ThunkDispatch<number, undefined, UnknownAction> | |
| >() | |
| configureStore({ | |
| reducer: () => 0, | |
| // @ts-expect-error | |
| enhancers: () => new Tuple('not a store enhancer'), | |
| }) | |
| const somePropertyStoreEnhancer: StoreEnhancer<{ | |
| someProperty: string | |
| }> = (next) => { | |
| return (reducer, preloadedState) => { | |
| return { | |
| ...next(reducer, preloadedState), | |
| someProperty: 'some value', | |
| } | |
| } | |
| } | |
| const anotherPropertyStoreEnhancer: StoreEnhancer<{ | |
| anotherProperty: number | |
| }> = (next) => { | |
| return (reducer, preloadedState) => { | |
| return { | |
| ...next(reducer, preloadedState), | |
| anotherProperty: 123, | |
| } | |
| } | |
| } | |
| const store3 = configureStore({ | |
| reducer: () => 0, | |
| enhancers: () => | |
| new Tuple(somePropertyStoreEnhancer, anotherPropertyStoreEnhancer), | |
| }) | |
| expectTypeOf(store3.dispatch).toEqualTypeOf<Dispatch>() | |
| expectTypeOf(store3.someProperty).toBeString() | |
| expectTypeOf(store3.anotherProperty).toBeNumber() | |
| const storeWithCallback = configureStore({ | |
| reducer: () => 0, | |
| enhancers: (getDefaultEnhancers) => | |
| getDefaultEnhancers() | |
| .prepend(anotherPropertyStoreEnhancer) | |
| .concat(somePropertyStoreEnhancer), | |
| }) | |
| expectTypeOf(store3.dispatch).toMatchTypeOf< | |
| Dispatch & ThunkDispatch<number, undefined, UnknownAction> | |
| >() | |
| expectTypeOf(store3.someProperty).toBeString() | |
| expectTypeOf(store3.anotherProperty).toBeNumber() | |
| const someStateExtendingEnhancer: StoreEnhancer< | |
| {}, | |
| { someProperty: string } | |
| > = | |
| (next) => | |
| (...args) => { | |
| const store = next(...args) | |
| const getState = () => ({ | |
| ...store.getState(), | |
| someProperty: 'some value', | |
| }) | |
| return { | |
| ...store, | |
| getState, | |
| } as any | |
| } | |
| const anotherStateExtendingEnhancer: StoreEnhancer< | |
| {}, | |
| { anotherProperty: number } | |
| > = | |
| (next) => | |
| (...args) => { | |
| const store = next(...args) | |
| const getState = () => ({ | |
| ...store.getState(), | |
| anotherProperty: 123, | |
| }) | |
| return { | |
| ...store, | |
| getState, | |
| } as any | |
| } | |
| const store4 = configureStore({ | |
| reducer: () => ({ aProperty: 0 }), | |
| enhancers: () => | |
| new Tuple(someStateExtendingEnhancer, anotherStateExtendingEnhancer), | |
| }) | |
| const state = store4.getState() | |
| expectTypeOf(state.aProperty).toBeNumber() | |
| expectTypeOf(state.someProperty).toBeString() | |
| expectTypeOf(state.anotherProperty).toBeNumber() | |
| const storeWithCallback2 = configureStore({ | |
| reducer: () => ({ aProperty: 0 }), | |
| enhancers: (gDE) => | |
| gDE().concat(someStateExtendingEnhancer, anotherStateExtendingEnhancer), | |
| }) | |
| const stateWithCallback = storeWithCallback2.getState() | |
| expectTypeOf(stateWithCallback.aProperty).toBeNumber() | |
| expectTypeOf(stateWithCallback.someProperty).toBeString() | |
| expectTypeOf(stateWithCallback.anotherProperty).toBeNumber() | |
| }) | |
| test('Preloaded state typings', () => { | |
| const counterReducer1: Reducer<number> = () => 0 | |
| const counterReducer2: Reducer<number> = () => 0 | |
| test('partial preloaded state', () => { | |
| const store = configureStore({ | |
| reducer: { | |
| counter1: counterReducer1, | |
| counter2: counterReducer2, | |
| }, | |
| preloadedState: { | |
| counter1: 0, | |
| }, | |
| }) | |
| expectTypeOf(store.getState().counter1).toBeNumber() | |
| expectTypeOf(store.getState().counter2).toBeNumber() | |
| }) | |
| test('empty preloaded state', () => { | |
| const store = configureStore({ | |
| reducer: { | |
| counter1: counterReducer1, | |
| counter2: counterReducer2, | |
| }, | |
| preloadedState: {}, | |
| }) | |
| expectTypeOf(store.getState().counter1).toBeNumber() | |
| expectTypeOf(store.getState().counter2).toBeNumber() | |
| }) | |
| test('excess properties in preloaded state', () => { | |
| const store = configureStore({ | |
| reducer: { | |
| // @ts-expect-error | |
| counter1: counterReducer1, | |
| counter2: counterReducer2, | |
| }, | |
| preloadedState: { | |
| counter1: 0, | |
| counter3: 5, | |
| }, | |
| }) | |
| expectTypeOf(store.getState().counter1).toBeNumber() | |
| expectTypeOf(store.getState().counter2).toBeNumber() | |
| }) | |
| test('mismatching properties in preloaded state', () => { | |
| const store = configureStore({ | |
| reducer: { | |
| // @ts-expect-error | |
| counter1: counterReducer1, | |
| counter2: counterReducer2, | |
| }, | |
| preloadedState: { | |
| counter3: 5, | |
| }, | |
| }) | |
| expectTypeOf(store.getState().counter1).toBeNumber() | |
| expectTypeOf(store.getState().counter2).toBeNumber() | |
| }) | |
| test('string preloaded state when expecting object', () => { | |
| const store = configureStore({ | |
| reducer: { | |
| // @ts-expect-error | |
| counter1: counterReducer1, | |
| counter2: counterReducer2, | |
| }, | |
| preloadedState: 'test', | |
| }) | |
| expectTypeOf(store.getState().counter1).toBeNumber() | |
| expectTypeOf(store.getState().counter2).toBeNumber() | |
| }) | |
| 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, | |
| }, | |
| }, | |
| }) | |
| expectTypeOf(store.getState().group1.counter1).toBeNumber() | |
| expectTypeOf(store.getState().group1.counter2).toBeNumber() | |
| expectTypeOf(store.getState().group2.counter1).toBeNumber() | |
| expectTypeOf(store.getState().group2.counter2).toBeNumber() | |
| }) | |
| 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, | |
| }, | |
| }, | |
| }) | |
| expectTypeOf(store.getState().group1.counter1).toBeNumber() | |
| expectTypeOf(store.getState().group1.counter2).toBeNumber() | |
| expectTypeOf(store.getState().group2.counter1).toBeNumber() | |
| expectTypeOf(store.getState().group2.counter2).toBeNumber() | |
| }) | |
| }) | |
| test('Dispatch typings', () => { | |
| type StateA = number | |
| const reducerA = () => 0 | |
| const thunkA = () => { | |
| return (() => {}) as any as ThunkAction<Promise<'A'>, StateA, any, any> | |
| } | |
| type StateB = string | |
| const thunkB = () => { | |
| return (dispatch: Dispatch, getState: () => StateB) => {} | |
| } | |
| test('by default, dispatching Thunks is possible', () => { | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| }) | |
| store.dispatch(thunkA()) | |
| // @ts-expect-error | |
| store.dispatch(thunkB()) | |
| const res = store.dispatch((dispatch, getState) => { | |
| return 42 | |
| }) | |
| const action = store.dispatch({ type: 'foo' }) | |
| }) | |
| test('return type of thunks and actions is inferred correctly', () => { | |
| const slice = createSlice({ | |
| name: 'counter', | |
| initialState: { | |
| value: 0, | |
| }, | |
| reducers: { | |
| incrementByAmount: (state, action: PayloadAction<number>) => { | |
| state.value += action.payload | |
| }, | |
| }, | |
| }) | |
| const store = configureStore({ | |
| reducer: { | |
| counter: slice.reducer, | |
| }, | |
| }) | |
| const action = slice.actions.incrementByAmount(2) | |
| const dispatchResult = store.dispatch(action) | |
| expectTypeOf(dispatchResult).toMatchTypeOf<{ | |
| type: string | |
| payload: number | |
| }>() | |
| const promiseResult = store.dispatch(async (dispatch) => { | |
| return 42 | |
| }) | |
| expectTypeOf(promiseResult).toEqualTypeOf<Promise<number>>() | |
| const store2 = configureStore({ | |
| reducer: { | |
| counter: slice.reducer, | |
| }, | |
| middleware: (gDM) => | |
| gDM({ | |
| thunk: { | |
| extraArgument: 42, | |
| }, | |
| }), | |
| }) | |
| const dispatchResult2 = store2.dispatch(action) | |
| expectTypeOf(dispatchResult2).toMatchTypeOf<{ | |
| type: string | |
| payload: number | |
| }>() | |
| }) | |
| test('removing the Thunk Middleware', () => { | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: () => new Tuple(), | |
| }) | |
| expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkA()) | |
| expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB()) | |
| }) | |
| test('adding the thunk middleware by hand', () => { | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: () => new Tuple(thunk as ThunkMiddleware<StateA>), | |
| }) | |
| store.dispatch(thunkA()) | |
| // @ts-expect-error | |
| store.dispatch(thunkB()) | |
| }) | |
| test('custom middleware', () => { | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: () => | |
| new Tuple(0 as unknown as Middleware<(a: StateA) => boolean, StateA>), | |
| }) | |
| expectTypeOf(store.dispatch(5)).toBeBoolean() | |
| expectTypeOf(store.dispatch(5)).not.toBeString() | |
| }) | |
| test('multiple custom middleware', () => { | |
| const middleware = [] as any as Tuple< | |
| [ | |
| Middleware<(a: 'a') => 'A', StateA>, | |
| Middleware<(b: 'b') => 'B', StateA>, | |
| ThunkMiddleware<StateA>, | |
| ] | |
| > | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: () => middleware, | |
| }) | |
| expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>() | |
| expectTypeOf(store.dispatch('b')).toEqualTypeOf<'B'>() | |
| expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>() | |
| }) | |
| test('Accepts thunk with `unknown`, `undefined` or `null` ThunkAction extraArgument per default', () => { | |
| const store = configureStore({ reducer: {} }) | |
| // undefined is the default value for the ThunkMiddleware extraArgument | |
| store.dispatch(function () {} as ThunkAction< | |
| void, | |
| {}, | |
| undefined, | |
| UnknownAction | |
| >) | |
| // `null` for the `extra` generic was previously documented in the RTK "Advanced Tutorial", but | |
| // is a bad pattern and users should use `unknown` instead | |
| // @ts-expect-error | |
| store.dispatch(function () {} as ThunkAction< | |
| void, | |
| {}, | |
| null, | |
| UnknownAction | |
| >) | |
| // unknown is the best way to type a ThunkAction if you do not care | |
| // about the value of the extraArgument, as it will always work with every | |
| // ThunkMiddleware, no matter the actual extraArgument type | |
| store.dispatch(function () {} as ThunkAction< | |
| void, | |
| {}, | |
| unknown, | |
| UnknownAction | |
| >) | |
| // @ts-expect-error | |
| store.dispatch(function () {} as ThunkAction< | |
| void, | |
| {}, | |
| boolean, | |
| UnknownAction | |
| >) | |
| }) | |
| test('custom middleware and getDefaultMiddleware', () => { | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: (gDM) => | |
| gDM().prepend((() => {}) as any as Middleware< | |
| (a: 'a') => 'A', | |
| StateA | |
| >), | |
| }) | |
| expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>() | |
| expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>() | |
| expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB()) | |
| }) | |
| test('custom middleware and getDefaultMiddleware, using prepend', () => { | |
| const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = | |
| _anyMiddleware | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: (gDM) => { | |
| const concatenated = gDM().prepend(otherMiddleware) | |
| expectTypeOf(concatenated).toMatchTypeOf< | |
| ReadonlyArray< | |
| typeof otherMiddleware | ThunkMiddleware | Middleware<{}> | |
| > | |
| >() | |
| return concatenated | |
| }, | |
| }) | |
| expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>() | |
| expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>() | |
| expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB()) | |
| }) | |
| test('custom middleware and getDefaultMiddleware, using concat', () => { | |
| const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = | |
| _anyMiddleware | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: (gDM) => { | |
| const concatenated = gDM().concat(otherMiddleware) | |
| expectTypeOf(concatenated).toMatchTypeOf< | |
| ReadonlyArray< | |
| typeof otherMiddleware | ThunkMiddleware | Middleware<{}> | |
| > | |
| >() | |
| return concatenated | |
| }, | |
| }) | |
| expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>() | |
| expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>() | |
| expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB()) | |
| }) | |
| test('middlewareBuilder notation, getDefaultMiddleware (unconfigured)', () => { | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: (getDefaultMiddleware) => | |
| getDefaultMiddleware().prepend((() => {}) as any as Middleware< | |
| (a: 'a') => 'A', | |
| StateA | |
| >), | |
| }) | |
| expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>() | |
| expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>() | |
| expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB()) | |
| }) | |
| test('middlewareBuilder notation, getDefaultMiddleware, concat & prepend', () => { | |
| const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = | |
| _anyMiddleware | |
| const otherMiddleware2: Middleware<(a: 'b') => 'B', StateA> = | |
| _anyMiddleware | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: (getDefaultMiddleware) => | |
| getDefaultMiddleware() | |
| .concat(otherMiddleware) | |
| .prepend(otherMiddleware2), | |
| }) | |
| expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>() | |
| expectTypeOf(store.dispatch(thunkA())).toEqualTypeOf<Promise<'A'>>() | |
| expectTypeOf(store.dispatch('b')).toEqualTypeOf<'B'>() | |
| expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkB()) | |
| }) | |
| test('middlewareBuilder notation, getDefaultMiddleware (thunk: false)', () => { | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: (getDefaultMiddleware) => | |
| getDefaultMiddleware({ thunk: false }).prepend( | |
| (() => {}) as any as Middleware<(a: 'a') => 'A', StateA>, | |
| ), | |
| }) | |
| expectTypeOf(store.dispatch('a')).toEqualTypeOf<'A'>() | |
| expectTypeOf(store.dispatch).parameter(0).not.toMatchTypeOf(thunkA()) | |
| }) | |
| test("badly typed middleware won't make `dispatch` `any`", () => { | |
| const store = configureStore({ | |
| reducer: reducerA, | |
| middleware: (getDefaultMiddleware) => | |
| getDefaultMiddleware().concat(_anyMiddleware as Middleware<any>), | |
| }) | |
| expectTypeOf(store.dispatch).not.toBeAny() | |
| }) | |
| test("decorated `configureStore` won't make `dispatch` `never`", () => { | |
| const someSlice = createSlice({ | |
| name: 'something', | |
| initialState: null as any, | |
| reducers: { | |
| set(state) { | |
| return state | |
| }, | |
| }, | |
| }) | |
| function configureMyStore<S>( | |
| options: Omit<ConfigureStoreOptions<S>, 'reducer'>, | |
| ) { | |
| return configureStore({ | |
| ...options, | |
| reducer: someSlice.reducer, | |
| }) | |
| } | |
| const store = configureMyStore({}) | |
| expectTypeOf(store.dispatch).toBeFunction() | |
| }) | |
| interface CounterState { | |
| value: number | |
| } | |
| const counterSlice = createSlice({ | |
| name: 'counter', | |
| initialState: { value: 0 } as CounterState, | |
| reducers: { | |
| increment(state) { | |
| state.value += 1 | |
| }, | |
| decrement(state) { | |
| state.value -= 1 | |
| }, | |
| // Use the PayloadAction type to declare the contents of `action.payload` | |
| incrementByAmount: (state, action: PayloadAction<number>) => { | |
| state.value += action.payload | |
| }, | |
| }, | |
| }) | |
| type Unsubscribe = () => void | |
| // A fake middleware that tells TS that an unsubscribe callback is being returned for a given action | |
| // This is the same signature that the "listener" middleware uses | |
| const dummyMiddleware: Middleware< | |
| { | |
| (action: Action<'actionListenerMiddleware/add'>): Unsubscribe | |
| }, | |
| CounterState | |
| > = (storeApi) => (next) => (action) => {} | |
| const store = configureStore({ | |
| reducer: counterSlice.reducer, | |
| middleware: (gDM) => gDM().prepend(dummyMiddleware), | |
| }) | |
| // Order matters here! We need the listener type to come first, otherwise | |
| // the thunk middleware type kicks in and TS thinks a plain action is being returned | |
| expectTypeOf(store.dispatch).toEqualTypeOf< | |
| ((action: Action<'actionListenerMiddleware/add'>) => Unsubscribe) & | |
| ThunkDispatch<CounterState, undefined, UnknownAction> & | |
| Dispatch<UnknownAction> | |
| >() | |
| const unsubscribe = store.dispatch({ | |
| type: 'actionListenerMiddleware/add', | |
| } as const) | |
| expectTypeOf(unsubscribe).toEqualTypeOf<Unsubscribe>() | |
| }) | |
| }) | |