996 lines
29 KiB
Plaintext
996 lines
29 KiB
Plaintext
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 } })
|
|
})
|
|
})
|