UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

996 lines (828 loc) 29.4 kB
import type { Action, ActionCreatorWithNonInferrablePayload, ActionCreatorWithOptionalPayload, ActionCreatorWithPayload, ActionCreatorWithPreparedPayload, ActionCreatorWithoutPayload, ActionReducerMapBuilder, AsyncThunk, CaseReducer, PayloadAction, PayloadActionCreator, Reducer, ReducerCreators, SerializedError, SliceCaseReducers, ThunkDispatch, UnknownAction, ValidateSliceCaseReducers, } from '@reduxjs/toolkit' import { asyncThunkCreator, buildCreateSlice, configureStore, createAction, createAsyncThunk, createSlice, isRejected, } from '@reduxjs/toolkit' import { castDraft } from 'immer' describe('type tests', () => { const counterSlice = createSlice({ name: 'counter', initialState: 0, reducers: { increment: (state: number, action) => state + action.payload, decrement: (state: number, action) => state - action.payload, }, }) test('Slice name is strongly typed.', () => { const uiSlice = createSlice({ name: 'ui', initialState: 0, reducers: { goToNext: (state: number, action) => state + action.payload, goToPrevious: (state: number, action) => state - action.payload, }, }) const actionCreators = { [counterSlice.name]: { ...counterSlice.actions }, [uiSlice.name]: { ...uiSlice.actions }, } expectTypeOf(counterSlice.actions).toEqualTypeOf(actionCreators.counter) expectTypeOf(uiSlice.actions).toEqualTypeOf(actionCreators.ui) expectTypeOf(actionCreators).not.toHaveProperty('anyKey') }) test("createSlice() infers the returned slice's type.", () => { const firstAction = createAction<{ count: number }>('FIRST_ACTION') const slice = createSlice({ name: 'counter', initialState: 0, reducers: { increment: (state: number, action) => state + action.payload, decrement: (state: number, action) => state - action.payload, }, extraReducers: (builder) => { builder.addCase( firstAction, (state, action) => state + action.payload.count, ) }, }) test('Reducer', () => { expectTypeOf(slice.reducer).toMatchTypeOf< Reducer<number, PayloadAction> >() expectTypeOf(slice.reducer).not.toMatchTypeOf< Reducer<string, PayloadAction> >() }) test('Actions', () => { slice.actions.increment(1) slice.actions.decrement(1) expectTypeOf(slice.actions).not.toHaveProperty('other') }) }) test('Slice action creator types are inferred.', () => { const counter = createSlice({ name: 'counter', initialState: 0, reducers: { increment: (state) => state + 1, decrement: ( state, { payload = 1 }: PayloadAction<number | undefined>, ) => state - payload, multiply: (state, { payload }: PayloadAction<number | number[]>) => Array.isArray(payload) ? payload.reduce((acc, val) => acc * val, state) : state * payload, addTwo: { reducer: (s, { payload }: PayloadAction<number>) => s + payload, prepare: (a: number, b: number) => ({ payload: a + b, }), }, }, }) expectTypeOf( counter.actions.increment, ).toMatchTypeOf<ActionCreatorWithoutPayload>() counter.actions.increment() expectTypeOf(counter.actions.decrement).toMatchTypeOf< ActionCreatorWithOptionalPayload<number | undefined> >() counter.actions.decrement() counter.actions.decrement(2) expectTypeOf(counter.actions.multiply).toMatchTypeOf< ActionCreatorWithPayload<number | number[]> >() counter.actions.multiply(2) counter.actions.multiply([2, 3, 4]) expectTypeOf(counter.actions.addTwo).toMatchTypeOf< ActionCreatorWithPreparedPayload<[number, number], number> >() counter.actions.addTwo(1, 2) expectTypeOf(counter.actions.multiply).parameters.not.toMatchTypeOf<[]>() expectTypeOf(counter.actions.multiply).parameter(0).not.toBeString() expectTypeOf(counter.actions.addTwo).parameters.not.toMatchTypeOf< [number] >() expectTypeOf(counter.actions.addTwo).parameters.toEqualTypeOf< [number, number] >() }) test('Slice action creator types properties are strongly typed', () => { const counter = createSlice({ name: 'counter', initialState: 0, reducers: { increment: (state) => state + 1, decrement: (state) => state - 1, multiply: (state, { payload }: PayloadAction<number | number[]>) => Array.isArray(payload) ? payload.reduce((acc, val) => acc * val, state) : state * payload, }, }) expectTypeOf( counter.actions.increment.type, ).toEqualTypeOf<'counter/increment'>() expectTypeOf( counter.actions.increment().type, ).toEqualTypeOf<'counter/increment'>() expectTypeOf( counter.actions.decrement.type, ).toEqualTypeOf<'counter/decrement'>() expectTypeOf( counter.actions.decrement().type, ).toEqualTypeOf<'counter/decrement'>() expectTypeOf( counter.actions.multiply.type, ).toEqualTypeOf<'counter/multiply'>() expectTypeOf( counter.actions.multiply(1).type, ).toEqualTypeOf<'counter/multiply'>() expectTypeOf( counter.actions.increment.type, ).not.toMatchTypeOf<'increment'>() }) test('Slice action creator types are inferred for enhanced reducers.', () => { const counter = createSlice({ name: 'test', initialState: { counter: 0, concat: '' }, reducers: { incrementByStrLen: { reducer: (state, action: PayloadAction<number>) => { state.counter += action.payload }, prepare: (payload: string) => ({ payload: payload.length, }), }, concatMetaStrLen: { reducer: (state, action: PayloadAction<string>) => { state.concat += action.payload }, prepare: (payload: string) => ({ payload, meta: payload.length, }), }, }, }) expectTypeOf( counter.actions.incrementByStrLen('test').type, ).toEqualTypeOf<'test/incrementByStrLen'>() expectTypeOf(counter.actions.incrementByStrLen('test').payload).toBeNumber() expectTypeOf(counter.actions.concatMetaStrLen('test').payload).toBeString() expectTypeOf(counter.actions.concatMetaStrLen('test').meta).toBeNumber() expectTypeOf( counter.actions.incrementByStrLen('test').payload, ).not.toBeString() expectTypeOf(counter.actions.concatMetaStrLen('test').meta).not.toBeString() }) test('access meta and error from reducer', () => { const counter = createSlice({ name: 'test', initialState: { counter: 0, concat: '' }, reducers: { // case: meta and error not used in reducer testDefaultMetaAndError: { reducer(_, action: PayloadAction<number, string>) {}, prepare: (payload: number) => ({ payload, meta: 'meta' as 'meta', error: 'error' as 'error', }), }, // case: meta and error marked as "unknown" in reducer testUnknownMetaAndError: { reducer( _, action: PayloadAction<number, string, unknown, unknown>, ) {}, prepare: (payload: number) => ({ payload, meta: 'meta' as 'meta', error: 'error' as 'error', }), }, // case: meta and error are typed in the reducer as returned by prepare testMetaAndError: { reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {}, prepare: (payload: number) => ({ payload, meta: 'meta' as 'meta', error: 'error' as 'error', }), }, // case: meta is typed differently in the reducer than returned from prepare testErroneousMeta: { reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {}, // @ts-expect-error prepare: (payload: number) => ({ payload, meta: 1, error: 'error' as 'error', }), }, // case: error is typed differently in the reducer than returned from prepare testErroneousError: { reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {}, // @ts-expect-error prepare: (payload: number) => ({ payload, meta: 'meta' as 'meta', error: 1, }), }, }, }) }) test('returned case reducer has the correct type', () => { const counter = createSlice({ name: 'counter', initialState: 0, reducers: { increment(state, action: PayloadAction<number>) { return state + action.payload }, decrement: { reducer(state, action: PayloadAction<number>) { return state - action.payload }, prepare(amount: number) { return { payload: amount } }, }, }, }) test('Should match positively', () => { expectTypeOf(counter.caseReducers.increment).toMatchTypeOf< (state: number, action: PayloadAction<number>) => number | void >() }) test('Should match positively for reducers with prepare callback', () => { expectTypeOf(counter.caseReducers.decrement).toMatchTypeOf< (state: number, action: PayloadAction<number>) => number | void >() }) test("Should not mismatch the payload if it's a simple reducer", () => { expectTypeOf(counter.caseReducers.increment).not.toMatchTypeOf< (state: number, action: PayloadAction<string>) => number | void >() }) test("Should not mismatch the payload if it's a reducer with a prepare callback", () => { expectTypeOf(counter.caseReducers.decrement).not.toMatchTypeOf< (state: number, action: PayloadAction<string>) => number | void >() }) test("Should not include entries that don't exist", () => { expectTypeOf(counter.caseReducers).not.toHaveProperty( 'someThingNonExistent', ) }) }) test('prepared payload does not match action payload - should cause an error.', () => { const counter = createSlice({ name: 'counter', initialState: { counter: 0 }, reducers: { increment: { reducer(state, action: PayloadAction<string>) { state.counter += action.payload.length }, // @ts-expect-error prepare(x: string) { return { payload: 6, } }, }, }, }) }) test('if no Payload Type is specified, accept any payload', () => { // see https://github.com/reduxjs/redux-toolkit/issues/165 const initialState = { name: null, } const mySlice = createSlice({ name: 'name', initialState, reducers: { setName: (state, action) => { state.name = action.payload }, }, }) expectTypeOf( mySlice.actions.setName, ).toMatchTypeOf<ActionCreatorWithNonInferrablePayload>() const x = mySlice.actions.setName mySlice.actions.setName(null) mySlice.actions.setName('asd') mySlice.actions.setName(5) }) test('actions.x.match()', () => { const mySlice = createSlice({ name: 'name', initialState: { name: 'test' }, reducers: { setName: (state, action: PayloadAction<string>) => { state.name = action.payload }, }, }) const x: Action<string> = {} as any if (mySlice.actions.setName.match(x)) { expectTypeOf(x.type).toEqualTypeOf<'name/setName'>() expectTypeOf(x.payload).toBeString() } else { expectTypeOf(x.type).not.toMatchTypeOf<'name/setName'>() expectTypeOf(x).not.toHaveProperty('payload') } }) test('builder callback for extraReducers', () => { createSlice({ name: 'test', initialState: 0, reducers: {}, extraReducers: (builder) => { expectTypeOf(builder).toEqualTypeOf<ActionReducerMapBuilder<number>>() }, }) }) test('wrapping createSlice should be possible', () => { interface GenericState<T> { data?: T status: 'loading' | 'finished' | 'error' } const createGenericSlice = < T, Reducers extends SliceCaseReducers<GenericState<T>>, >({ name = '', initialState, reducers, }: { name: string initialState: GenericState<T> reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers> }) => { return createSlice({ name, initialState, reducers: { start(state) { state.status = 'loading' }, success(state: GenericState<T>, action: PayloadAction<T>) { state.data = action.payload state.status = 'finished' }, ...reducers, }, }) } const wrappedSlice = createGenericSlice({ name: 'test', initialState: { status: 'loading' } as GenericState<string>, reducers: { magic(state) { expectTypeOf(state).toEqualTypeOf<GenericState<string>>() expectTypeOf(state).not.toMatchTypeOf<GenericState<number>>() state.status = 'finished' state.data = 'hocus pocus' }, }, }) expectTypeOf(wrappedSlice.actions.success).toMatchTypeOf< ActionCreatorWithPayload<string> >() expectTypeOf(wrappedSlice.actions.magic).toMatchTypeOf< ActionCreatorWithoutPayload<string> >() }) test('extraReducers', () => { interface GenericState<T> { data: T | null } function createDataSlice< T, Reducers extends SliceCaseReducers<GenericState<T>>, >( name: string, reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>, initialState: GenericState<T>, ) { const doNothing = createAction<undefined>('doNothing') const setData = createAction<T>('setData') const slice = createSlice({ name, initialState, reducers, extraReducers: (builder) => { builder.addCase(doNothing, (state) => { return { ...state } }) builder.addCase(setData, (state, { payload }) => { return { ...state, data: payload, } }) }, }) return { doNothing, setData, slice } } }) test('slice selectors', () => { const sliceWithoutSelectors = createSlice({ name: '', initialState: '', reducers: {}, }) expectTypeOf(sliceWithoutSelectors.selectors).not.toHaveProperty('foo') const sliceWithSelectors = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1 }, }, selectors: { selectValue: (state) => state.value, selectMultiply: (state, multiplier: number) => state.value * multiplier, selectToFixed: Object.assign( (state: { value: number }) => state.value.toFixed(2), { static: true }, ), }, }) const rootState = { [sliceWithSelectors.reducerPath]: sliceWithSelectors.getInitialState(), } const { selectValue, selectMultiply, selectToFixed } = sliceWithSelectors.selectors expectTypeOf(selectValue(rootState)).toBeNumber() expectTypeOf(selectMultiply(rootState, 2)).toBeNumber() expectTypeOf(selectToFixed(rootState)).toBeString() expectTypeOf(selectToFixed.unwrapped.static).toBeBoolean() const nestedState = { nested: rootState, } const nestedSelectors = sliceWithSelectors.getSelectors( (rootState: typeof nestedState) => rootState.nested.counter, ) expectTypeOf(nestedSelectors.selectValue(nestedState)).toBeNumber() expectTypeOf(nestedSelectors.selectMultiply(nestedState, 2)).toBeNumber() expectTypeOf(nestedSelectors.selectToFixed(nestedState)).toBeString() }) test('reducer callback', () => { interface TestState { foo: string } interface TestArg { test: string } interface TestReturned { payload: string } interface TestReject { cause: string } const slice = createSlice({ name: 'test', initialState: {} as TestState, reducers: (create) => { const preTypedAsyncThunk = create.asyncThunk.withTypes<{ rejectValue: TestReject }>() // @ts-expect-error create.asyncThunk<any, any, { state: StoreState }>(() => {}) // @ts-expect-error create.asyncThunk.withTypes<{ rejectValue: string dispatch: StoreDispatch }>() return { normalReducer: create.reducer<string>((state, action) => { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.payload).toBeString() }), optionalReducer: create.reducer<string | undefined>( (state, action) => { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.payload).toEqualTypeOf<string | undefined>() }, ), noActionReducer: create.reducer((state) => { expectTypeOf(state).toEqualTypeOf<TestState>() }), preparedReducer: create.preparedReducer( (payload: string) => ({ payload, meta: 'meta' as const, error: 'error' as const, }), (state, action) => { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.payload).toBeString() expectTypeOf(action.meta).toEqualTypeOf<'meta'>() expectTypeOf(action.error).toEqualTypeOf<'error'>() }, ), testInferVoid: create.asyncThunk(() => {}, { pending(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toBeVoid() }, fulfilled(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toBeVoid() expectTypeOf(action.payload).toBeVoid() }, rejected(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toBeVoid() expectTypeOf(action.error).toEqualTypeOf<SerializedError>() }, settled(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toBeVoid() if (isRejected(action)) { expectTypeOf(action.error).toEqualTypeOf<SerializedError>() } else { expectTypeOf(action.payload).toBeVoid() } }, }), testInfer: create.asyncThunk( function payloadCreator(arg: TestArg, api) { return Promise.resolve<TestReturned>({ payload: 'foo' }) }, { pending(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() }, fulfilled(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() expectTypeOf(action.payload).toEqualTypeOf<TestReturned>() }, rejected(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() expectTypeOf(action.error).toEqualTypeOf<SerializedError>() }, settled(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() if (isRejected(action)) { expectTypeOf(action.error).toEqualTypeOf<SerializedError>() } else { expectTypeOf(action.payload).toEqualTypeOf<TestReturned>() } }, }, ), testExplicitType: create.asyncThunk< TestReturned, TestArg, { rejectValue: TestReject } >( function payloadCreator(arg, api) { // here would be a circular reference expectTypeOf(api.getState()).toBeUnknown() // here would be a circular reference expectTypeOf(api.dispatch).toMatchTypeOf< ThunkDispatch<any, any, any> >() // so you need to cast inside instead const getState = api.getState as () => StoreState const dispatch = api.dispatch as StoreDispatch expectTypeOf(arg).toEqualTypeOf<TestArg>() expectTypeOf(api.rejectWithValue).toMatchTypeOf< (value: TestReject) => any >() return Promise.resolve({ payload: 'foo' }) }, { pending(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() }, fulfilled(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() expectTypeOf(action.payload).toEqualTypeOf<TestReturned>() }, rejected(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() expectTypeOf(action.error).toEqualTypeOf<SerializedError>() expectTypeOf(action.payload).toEqualTypeOf< TestReject | undefined >() }, settled(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() if (isRejected(action)) { expectTypeOf(action.error).toEqualTypeOf<SerializedError>() expectTypeOf(action.payload).toEqualTypeOf< TestReject | undefined >() } else { expectTypeOf(action.payload).toEqualTypeOf<TestReturned>() } }, }, ), testPreTyped: preTypedAsyncThunk( function payloadCreator(arg: TestArg, api) { expectTypeOf(api.rejectWithValue).toMatchTypeOf< (value: TestReject) => any >() return Promise.resolve<TestReturned>({ payload: 'foo' }) }, { pending(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() }, fulfilled(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() expectTypeOf(action.payload).toEqualTypeOf<TestReturned>() }, rejected(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() expectTypeOf(action.error).toEqualTypeOf<SerializedError>() expectTypeOf(action.payload).toEqualTypeOf< TestReject | undefined >() }, settled(state, action) { expectTypeOf(state).toEqualTypeOf<TestState>() expectTypeOf(action.meta.arg).toEqualTypeOf<TestArg>() if (isRejected(action)) { expectTypeOf(action.error).toEqualTypeOf<SerializedError>() expectTypeOf(action.payload).toEqualTypeOf< TestReject | undefined >() } else { expectTypeOf(action.payload).toEqualTypeOf<TestReturned>() } }, }, ), } }, }) const store = configureStore({ reducer: { test: slice.reducer } }) type StoreState = ReturnType<typeof store.getState> type StoreDispatch = typeof store.dispatch expectTypeOf(slice.actions.normalReducer).toMatchTypeOf< PayloadActionCreator<string> >() expectTypeOf(slice.actions.normalReducer).toBeCallableWith('') expectTypeOf(slice.actions.normalReducer).parameters.not.toMatchTypeOf<[]>() expectTypeOf(slice.actions.normalReducer).parameters.not.toMatchTypeOf< [number] >() expectTypeOf(slice.actions.optionalReducer).toMatchTypeOf< ActionCreatorWithOptionalPayload<string | undefined> >() expectTypeOf(slice.actions.optionalReducer).toBeCallableWith() expectTypeOf(slice.actions.optionalReducer).toBeCallableWith('') expectTypeOf(slice.actions.optionalReducer).parameter(0).not.toBeNumber() expectTypeOf( slice.actions.noActionReducer, ).toMatchTypeOf<ActionCreatorWithoutPayload>() expectTypeOf(slice.actions.noActionReducer).toBeCallableWith() expectTypeOf(slice.actions.noActionReducer).parameter(0).not.toBeString() expectTypeOf(slice.actions.preparedReducer).toEqualTypeOf< ActionCreatorWithPreparedPayload< [string], string, 'test/preparedReducer', 'error', 'meta' > >() expectTypeOf(slice.actions.testInferVoid).toEqualTypeOf< AsyncThunk<void, void, {}> >() expectTypeOf(slice.actions.testInferVoid).toBeCallableWith() expectTypeOf(slice.actions.testInfer).toEqualTypeOf< AsyncThunk<TestReturned, TestArg, {}> >() expectTypeOf(slice.actions.testExplicitType).toEqualTypeOf< AsyncThunk<TestReturned, TestArg, { rejectValue: TestReject }> >() type TestInferThunk = AsyncThunk<TestReturned, TestArg, {}> expectTypeOf(slice.caseReducers.testInfer.pending).toEqualTypeOf< CaseReducer<TestState, ReturnType<TestInferThunk['pending']>> >() expectTypeOf(slice.caseReducers.testInfer.fulfilled).toEqualTypeOf< CaseReducer<TestState, ReturnType<TestInferThunk['fulfilled']>> >() expectTypeOf(slice.caseReducers.testInfer.rejected).toEqualTypeOf< CaseReducer<TestState, ReturnType<TestInferThunk['rejected']>> >() }) test('wrapping createSlice should be possible, with callback', () => { interface GenericState<T> { data?: T status: 'loading' | 'finished' | 'error' } const createGenericSlice = < T, Reducers extends SliceCaseReducers<GenericState<T>>, >({ name = '', initialState, reducers, }: { name: string initialState: GenericState<T> reducers: (create: ReducerCreators<GenericState<T>>) => Reducers }) => { return createSlice({ name, initialState, reducers: (create) => ({ start: create.reducer((state) => { state.status = 'loading' }), success: create.reducer<T>((state, action) => { state.data = castDraft(action.payload) state.status = 'finished' }), ...reducers(create), }), }) } const wrappedSlice = createGenericSlice({ name: 'test', initialState: { status: 'loading' } as GenericState<string>, reducers: (create) => ({ magic: create.reducer((state) => { expectTypeOf(state).toEqualTypeOf<GenericState<string>>() expectTypeOf(state).not.toMatchTypeOf<GenericState<number>>() state.status = 'finished' state.data = 'hocus pocus' }), }), }) expectTypeOf(wrappedSlice.actions.success).toMatchTypeOf< ActionCreatorWithPayload<string> >() expectTypeOf(wrappedSlice.actions.magic).toMatchTypeOf< ActionCreatorWithoutPayload<string> >() }) test('selectSlice', () => { expectTypeOf(counterSlice.selectSlice({ counter: 0 })).toBeNumber() // We use `not.toEqualTypeOf` instead of `not.toMatchTypeOf` // because `toMatchTypeOf` allows missing properties expectTypeOf(counterSlice.selectSlice).parameter(0).not.toEqualTypeOf<{}>() }) test('buildCreateSlice', () => { expectTypeOf(buildCreateSlice()).toEqualTypeOf(createSlice) buildCreateSlice({ // @ts-expect-error not possible to recreate shape because symbol is not exported creators: { asyncThunk: { [Symbol()]: createAsyncThunk } }, }) buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } }) }) })