import type { SerializedError } from '@internal/createAsyncThunk' import { createAsyncThunk } from '@internal/createAsyncThunk' import { executeReducerBuilderCallback } from '@internal/mapBuilders' import type { UnknownAction } from '@reduxjs/toolkit' import { createAction } from '@reduxjs/toolkit' describe('type tests', () => { test('builder callback for actionMap', () => { const increment = createAction('increment') const decrement = createAction('decrement') executeReducerBuilderCallback((builder) => { builder.addCase(increment, (state, action) => { expectTypeOf(state).toBeNumber() expectTypeOf(action).toEqualTypeOf<{ type: 'increment' payload: number }>() expectTypeOf(state).not.toBeString() expectTypeOf(action).not.toMatchTypeOf<{ type: 'increment' payload: string }>() expectTypeOf(action).not.toMatchTypeOf<{ type: 'decrement' payload: number }>() }) builder.addCase('increment', (state, action) => { expectTypeOf(state).toBeNumber() expectTypeOf(action).toEqualTypeOf<{ type: 'increment' }>() expectTypeOf(state).not.toBeString() expectTypeOf(action).not.toMatchTypeOf<{ type: 'decrement' }>() // this cannot be inferred and has to be manually specified expectTypeOf(action).not.toMatchTypeOf<{ type: 'increment' payload: number }>() }) builder.addCase( increment, (state, action: ReturnType) => state, ) // @ts-expect-error builder.addCase( increment, (state, action: ReturnType) => state, ) builder.addCase( 'increment', (state, action: ReturnType) => state, ) // @ts-expect-error builder.addCase( 'decrement', (state, action: ReturnType) => state, ) // action type is inferred builder.addMatcher(increment.match, (state, action) => { expectTypeOf(action).toEqualTypeOf>() }) test('action type is inferred when type predicate lacks `type` property', () => { type PredicateWithoutTypeProperty = { payload: number } builder.addMatcher( (action): action is PredicateWithoutTypeProperty => true, (state, action) => { expectTypeOf(action).toMatchTypeOf() expectTypeOf(action).toMatchTypeOf() }, ) }) // action type defaults to UnknownAction if no type predicate matcher is passed builder.addMatcher( () => true, (state, action) => { expectTypeOf(action).toMatchTypeOf() }, ) // with a boolean checker, action can also be typed by type argument builder.addMatcher<{ foo: boolean }>( () => true, (state, action) => { expectTypeOf(action).toMatchTypeOf<{ foo: boolean }>() expectTypeOf(action).toMatchTypeOf() }, ) // addCase().addMatcher() is possible, action type inferred correctly builder .addCase( 'increment', (state, action: ReturnType) => state, ) .addMatcher(decrement.match, (state, action) => { expectTypeOf(action).toEqualTypeOf>() }) // addCase().addDefaultCase() is possible, action type is UnknownAction builder .addCase( 'increment', (state, action: ReturnType) => state, ) .addDefaultCase((state, action) => { expectTypeOf(action).toMatchTypeOf() }) test('addMatcher() should prevent further calls to addCase()', () => { const b = builder.addMatcher(increment.match, () => {}) expectTypeOf(b).not.toHaveProperty('addCase') expectTypeOf(b.addMatcher).toBeCallableWith(increment.match, () => {}) expectTypeOf(b.addDefaultCase).toBeCallableWith(() => {}) }) test('addDefaultCase() should prevent further calls to addCase(), addMatcher() and addDefaultCase', () => { const b = builder.addDefaultCase(() => {}) expectTypeOf(b).not.toHaveProperty('addCase') expectTypeOf(b).not.toHaveProperty('addMatcher') expectTypeOf(b).not.toHaveProperty('addDefaultCase') }) describe('`createAsyncThunk` actions work with `mapBuilder`', () => { test('case 1: normal `createAsyncThunk`', () => { const thunk = createAsyncThunk('test', () => { return 'ret' as const }) builder.addCase(thunk.pending, (_, action) => { expectTypeOf(action).toMatchTypeOf<{ payload: undefined meta: { arg: void requestId: string requestStatus: 'pending' } }>() }) builder.addCase(thunk.rejected, (_, action) => { expectTypeOf(action).toMatchTypeOf<{ payload: unknown error: SerializedError meta: { arg: void requestId: string requestStatus: 'rejected' aborted: boolean condition: boolean rejectedWithValue: boolean } }>() }) builder.addCase(thunk.fulfilled, (_, action) => { expectTypeOf(action).toMatchTypeOf<{ payload: 'ret' meta: { arg: void requestId: string requestStatus: 'fulfilled' } }>() }) }) }) test('case 2: `createAsyncThunk` with `meta`', () => { const thunk = createAsyncThunk< 'ret', void, { pendingMeta: { startedTimeStamp: number } fulfilledMeta: { fulfilledTimeStamp: number baseQueryMeta: 'meta!' } rejectedMeta: { baseQueryMeta: 'meta!' } } >( 'test', (_, api) => { return api.fulfillWithValue('ret' as const, { fulfilledTimeStamp: 5, baseQueryMeta: 'meta!', }) }, { getPendingMeta() { return { startedTimeStamp: 0 } }, }, ) builder.addCase(thunk.pending, (_, action) => { expectTypeOf(action).toMatchTypeOf<{ payload: undefined meta: { arg: void requestId: string requestStatus: 'pending' startedTimeStamp: number } }>() }) builder.addCase(thunk.rejected, (_, action) => { expectTypeOf(action).toMatchTypeOf<{ payload: unknown error: SerializedError meta: { arg: void requestId: string requestStatus: 'rejected' aborted: boolean condition: boolean rejectedWithValue: boolean baseQueryMeta?: 'meta!' } }>() if (action.meta.rejectedWithValue) { expectTypeOf(action.meta.baseQueryMeta).toEqualTypeOf<'meta!'>() } }) builder.addCase(thunk.fulfilled, (_, action) => { expectTypeOf(action).toMatchTypeOf<{ payload: 'ret' meta: { arg: void requestId: string requestStatus: 'fulfilled' baseQueryMeta: 'meta!' } }>() }) }) }) }) })