@reduxjs/toolkit
Version:
The official, opinionated, batteries-included toolset for efficient Redux development
406 lines (365 loc) • 10.3 kB
text/typescript
/* eslint-disable no-lone-blocks */
import type { Dispatch, AnyAction, Middleware, Reducer, Store } from 'redux'
import { applyMiddleware } from 'redux'
import type { PayloadAction } from '@reduxjs/toolkit'
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import type { ThunkMiddleware, ThunkAction } from 'redux-thunk'
import thunk, { ThunkDispatch } from 'redux-thunk'
import { expectNotAny, expectType } from './helpers'
const _anyMiddleware: any = () => () => () => {}
/*
* 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 })
const numberStore: Store<number, AnyAction> = store
// @ts-expect-error
const stringStore: Store<string, AnyAction> = store
}
/*
* Test: configureStore() infers the store action type.
*/
{
const reducer: Reducer<number, PayloadAction<number>> = () => 0
const store = configureStore({ reducer })
const numberStore: Store<number, PayloadAction<number>> = store
// @ts-expect-error
const stringStore: Store<number, PayloadAction<string>> = store
}
/*
* Test: configureStore() accepts middleware array.
*/
{
const middleware: Middleware = (store) => (next) => next
configureStore({
reducer: () => 0,
middleware: [middleware],
})
configureStore({
reducer: () => 0,
// @ts-expect-error
middleware: ['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({
reducer: () => 0,
// @ts-expect-error
preloadedState: 'non-matching state type',
})
}
/*
* Test: configureStore() accepts store enhancer.
*/
{
configureStore({
reducer: () => 0,
enhancers: [applyMiddleware((store) => (next) => next)],
})
configureStore({
reducer: () => 0,
// @ts-expect-error
enhancers: ['not a store enhancer'],
})
}
/**
* Test: configureStore() state type inference works when specifying both a
* reducer object and a partial preloaded state.
*/
{
let counterReducer1: Reducer<number> = () => 0
let counterReducer2: Reducer<number> = () => 0
const store = configureStore({
reducer: {
counter1: counterReducer1,
counter2: counterReducer2,
},
preloadedState: {
counter1: 0,
},
})
const counter1: number = store.getState().counter1
const counter2: number = store.getState().counter2
}
/**
* Test: Dispatch typings
*/
{
type StateA = number
const reducerA = () => 0
function thunkA() {
return (() => {}) as any as ThunkAction<Promise<'A'>, StateA, any, any>
}
type StateB = string
function 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())
}
/**
* Test: removing the Thunk Middleware
*/
{
const store = configureStore({
reducer: reducerA,
middleware: [],
})
// @ts-expect-error
store.dispatch(thunkA())
// @ts-expect-error
store.dispatch(thunkB())
}
/**
* Test: adding the thunk middleware by hand
*/
{
const store = configureStore({
reducer: reducerA,
middleware: [thunk] as [ThunkMiddleware<StateA>],
})
store.dispatch(thunkA())
// @ts-expect-error
store.dispatch(thunkB())
}
/**
* Test: using getDefaultMiddleware
*/
{
const store = configureStore({
reducer: reducerA,
middleware: getDefaultMiddleware<StateA>(),
})
store.dispatch(thunkA())
// @ts-expect-error
store.dispatch(thunkB())
}
/**
* Test: custom middleware
*/
{
const store = configureStore({
reducer: reducerA,
middleware: [] as any as [Middleware<(a: StateA) => boolean, StateA>],
})
const result: boolean = store.dispatch(5)
// @ts-expect-error
const result2: string = store.dispatch(5)
}
/**
* Test: multiple custom middleware
*/
{
const store = configureStore({
reducer: reducerA,
middleware: [] as any as [
Middleware<(a: 'a') => 'A', StateA>,
Middleware<(b: 'b') => 'B', StateA>,
ThunkMiddleware<StateA>
],
})
const result: 'A' = store.dispatch('a')
const result2: 'B' = store.dispatch('b')
const result3: Promise<'A'> = store.dispatch(thunkA())
}
/**
* 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,
AnyAction
>)
// null was previously documented in the redux docs
store.dispatch(function () {} as ThunkAction<void, {}, null, AnyAction>)
// 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, AnyAction>)
// @ts-expect-error
store.dispatch(function () {} as ThunkAction<void, {}, boolean, AnyAction>)
}
/**
* Test: custom middleware and getDefaultMiddleware
*/
{
const store = configureStore({
reducer: reducerA,
middleware: [
(() => {}) as any as Middleware<(a: 'a') => 'A', StateA>,
...getDefaultMiddleware<StateA>(),
] as const,
})
const result1: 'A' = store.dispatch('a')
const result2: Promise<'A'> = store.dispatch(thunkA())
// @ts-expect-error
store.dispatch(thunkB())
}
/**
* Test: custom middleware and getDefaultMiddleware, using prepend
*/
{
const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
const concatenated = getDefaultMiddleware<StateA>().prepend(otherMiddleware)
expectType<
ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>>
>(concatenated)
const store = configureStore({
reducer: reducerA,
middleware: concatenated,
})
const result1: 'A' = store.dispatch('a')
const result2: Promise<'A'> = store.dispatch(thunkA())
// @ts-expect-error
store.dispatch(thunkB())
}
/**
* Test: custom middleware and getDefaultMiddleware, using concat
*/
{
const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
const concatenated = getDefaultMiddleware<StateA>().concat(otherMiddleware)
expectType<
ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>>
>(concatenated)
const store = configureStore({
reducer: reducerA,
middleware: concatenated,
})
const result1: 'A' = store.dispatch('a')
const result2: Promise<'A'> = store.dispatch(thunkA())
// @ts-expect-error
store.dispatch(thunkB())
}
/**
* Test: middlewareBuilder notation, getDefaultMiddleware (unconfigured)
*/
{
const store = configureStore({
reducer: reducerA,
middleware: (getDefaultMiddleware) =>
[
(() => {}) as any as Middleware<(a: 'a') => 'A', StateA>,
...getDefaultMiddleware(),
] as const,
})
const result1: 'A' = store.dispatch('a')
const result2: Promise<'A'> = store.dispatch(thunkA())
// @ts-expect-error
store.dispatch(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),
})
const result1: 'A' = store.dispatch('a')
const result2: Promise<'A'> = store.dispatch(thunkA())
const result3: 'B' = store.dispatch('b')
// @ts-expect-error
store.dispatch(thunkB())
}
/**
* Test: middlewareBuilder notation, getDefaultMiddleware (thunk: false)
*/
{
const store = configureStore({
reducer: reducerA,
middleware: (getDefaultMiddleware) =>
[
(() => {}) as any as Middleware<(a: 'a') => 'A', StateA>,
...getDefaultMiddleware({ thunk: false }),
] as const,
})
const result1: 'A' = store.dispatch('a')
// @ts-expect-error
store.dispatch(thunkA())
}
/**
* Test: badly typed middleware won't make `dispatch` `any`
*/
{
const store = configureStore({
reducer: reducerA,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(_anyMiddleware as Middleware<any>),
})
expectNotAny(store.dispatch)
}
}