Build NapCat Artifacts / Windows Loader Test (push) Canceled after 0s
Build NapCat Artifacts / Type Check & Test (push) Canceled after 0s
Build NapCat Artifacts / Build NapCat.Framework (push) Canceled after 0s
Build NapCat Artifacts / Build NapCat.Shell (push) Canceled after 0s
900 lines
27 KiB
Plaintext
900 lines
27 KiB
Plaintext
import type {
|
|
AsyncThunk,
|
|
SerializedError,
|
|
ThunkDispatch,
|
|
UnknownAction,
|
|
} from '@reduxjs/toolkit'
|
|
import {
|
|
configureStore,
|
|
createAsyncThunk,
|
|
createReducer,
|
|
createSlice,
|
|
unwrapResult,
|
|
} from '@reduxjs/toolkit'
|
|
|
|
import type { TSVersion } from '@phryneas/ts-version'
|
|
import type { AxiosError } from 'axios'
|
|
import apiRequest from 'axios'
|
|
import type { AsyncThunkDispatchConfig } from '@internal/createAsyncThunk'
|
|
|
|
const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, UnknownAction>
|
|
const unknownAction = { type: 'foo' } as UnknownAction
|
|
|
|
describe('type tests', () => {
|
|
test('basic usage', async () => {
|
|
const asyncThunk = createAsyncThunk('test', (id: number) =>
|
|
Promise.resolve(id * 2),
|
|
)
|
|
|
|
const reducer = createReducer({}, (builder) =>
|
|
builder
|
|
.addCase(asyncThunk.pending, (_, action) => {
|
|
expectTypeOf(action).toEqualTypeOf<
|
|
ReturnType<(typeof asyncThunk)['pending']>
|
|
>()
|
|
})
|
|
|
|
.addCase(asyncThunk.fulfilled, (_, action) => {
|
|
expectTypeOf(action).toEqualTypeOf<
|
|
ReturnType<(typeof asyncThunk)['fulfilled']>
|
|
>()
|
|
|
|
expectTypeOf(action.payload).toBeNumber()
|
|
})
|
|
|
|
.addCase(asyncThunk.rejected, (_, action) => {
|
|
expectTypeOf(action).toEqualTypeOf<
|
|
ReturnType<(typeof asyncThunk)['rejected']>
|
|
>()
|
|
|
|
expectTypeOf(action.error).toMatchTypeOf<Partial<Error> | undefined>()
|
|
}),
|
|
)
|
|
|
|
const promise = defaultDispatch(asyncThunk(3))
|
|
|
|
expectTypeOf(promise.requestId).toBeString()
|
|
|
|
expectTypeOf(promise.arg).toBeNumber()
|
|
|
|
expectTypeOf(promise.abort).toEqualTypeOf<(reason?: string) => void>()
|
|
|
|
const result = await promise
|
|
|
|
if (asyncThunk.fulfilled.match(result)) {
|
|
expectTypeOf(result).toEqualTypeOf<
|
|
ReturnType<(typeof asyncThunk)['fulfilled']>
|
|
>()
|
|
} else {
|
|
expectTypeOf(result).toEqualTypeOf<
|
|
ReturnType<(typeof asyncThunk)['rejected']>
|
|
>()
|
|
}
|
|
|
|
promise
|
|
.then(unwrapResult)
|
|
.then((result) => {
|
|
expectTypeOf(result).toBeNumber()
|
|
|
|
expectTypeOf(result).not.toMatchTypeOf<Error>()
|
|
})
|
|
.catch((error) => {
|
|
// catch is always any-typed, nothing we can do here
|
|
expectTypeOf(error).toBeAny()
|
|
})
|
|
})
|
|
|
|
test('More complex usage of thunk args', () => {
|
|
interface BookModel {
|
|
id: string
|
|
title: string
|
|
}
|
|
|
|
type BooksState = BookModel[]
|
|
|
|
const fakeBooks: BookModel[] = [
|
|
{ id: 'b', title: 'Second' },
|
|
{ id: 'a', title: 'First' },
|
|
]
|
|
|
|
const correctDispatch = (() => {}) as ThunkDispatch<
|
|
BookModel[],
|
|
{ userAPI: Function },
|
|
UnknownAction
|
|
>
|
|
|
|
// Verify that the the first type args to createAsyncThunk line up right
|
|
const fetchBooksTAC = createAsyncThunk<
|
|
BookModel[],
|
|
number,
|
|
{
|
|
state: BooksState
|
|
extra: { userAPI: Function }
|
|
}
|
|
>(
|
|
'books/fetch',
|
|
async (arg, { getState, dispatch, extra, requestId, signal }) => {
|
|
const state = getState()
|
|
|
|
expectTypeOf(arg).toBeNumber()
|
|
|
|
expectTypeOf(state).toEqualTypeOf<BookModel[]>()
|
|
|
|
expectTypeOf(extra).toEqualTypeOf<{ userAPI: Function }>()
|
|
|
|
return fakeBooks
|
|
},
|
|
)
|
|
|
|
correctDispatch(fetchBooksTAC(1))
|
|
// @ts-expect-error
|
|
defaultDispatch(fetchBooksTAC(1))
|
|
})
|
|
|
|
test('returning a rejected action from the promise creator is possible', async () => {
|
|
type ReturnValue = { data: 'success' }
|
|
type RejectValue = { data: 'error' }
|
|
|
|
const fetchBooksTAC = createAsyncThunk<
|
|
ReturnValue,
|
|
number,
|
|
{
|
|
rejectValue: RejectValue
|
|
}
|
|
>('books/fetch', async (arg, { rejectWithValue }) => {
|
|
return rejectWithValue({ data: 'error' })
|
|
})
|
|
|
|
const returned = await defaultDispatch(fetchBooksTAC(1))
|
|
if (fetchBooksTAC.rejected.match(returned)) {
|
|
expectTypeOf(returned.payload).toEqualTypeOf<undefined | RejectValue>()
|
|
|
|
expectTypeOf(returned.payload).toBeNullable()
|
|
} else {
|
|
expectTypeOf(returned.payload).toEqualTypeOf<ReturnValue>()
|
|
}
|
|
|
|
expectTypeOf(unwrapResult(returned)).toEqualTypeOf<ReturnValue>()
|
|
|
|
expectTypeOf(unwrapResult(returned)).not.toMatchTypeOf<RejectValue>()
|
|
})
|
|
|
|
test('regression #1156: union return values fall back to allowing only single member', () => {
|
|
const fn = createAsyncThunk('session/isAdmin', async () => {
|
|
const response: boolean = false
|
|
return response
|
|
})
|
|
})
|
|
|
|
test('Should handle reject with value within a try catch block. Note: this is a sample code taken from #1605', () => {
|
|
type ResultType = {
|
|
text: string
|
|
}
|
|
const demoPromise = async (): Promise<ResultType> =>
|
|
new Promise((resolve, _) => resolve({ text: '' }))
|
|
const thunk = createAsyncThunk('thunk', async (args, thunkAPI) => {
|
|
try {
|
|
const result = await demoPromise()
|
|
return result
|
|
} catch (error) {
|
|
return thunkAPI.rejectWithValue(error)
|
|
}
|
|
})
|
|
createReducer({}, (builder) =>
|
|
builder.addCase(thunk.fulfilled, (s, action) => {
|
|
expectTypeOf(action.payload).toEqualTypeOf<ResultType>()
|
|
}),
|
|
)
|
|
})
|
|
|
|
test('reject with value', () => {
|
|
interface Item {
|
|
name: string
|
|
}
|
|
|
|
interface ErrorFromServer {
|
|
error: string
|
|
}
|
|
|
|
interface CallsResponse {
|
|
data: Item[]
|
|
}
|
|
|
|
const fetchLiveCallsError = createAsyncThunk<
|
|
Item[],
|
|
string,
|
|
{
|
|
rejectValue: ErrorFromServer
|
|
}
|
|
>('calls/fetchLiveCalls', async (organizationId, { rejectWithValue }) => {
|
|
try {
|
|
const result = await apiRequest.get<CallsResponse>(
|
|
`organizations/${organizationId}/calls/live/iwill404`,
|
|
)
|
|
return result.data.data
|
|
} catch (err) {
|
|
const error: AxiosError<ErrorFromServer> = err as any // cast for access to AxiosError properties
|
|
if (!error.response) {
|
|
// let it be handled as any other unknown error
|
|
throw err
|
|
}
|
|
return rejectWithValue(error.response && error.response.data)
|
|
}
|
|
})
|
|
|
|
defaultDispatch(fetchLiveCallsError('asd')).then((result) => {
|
|
if (fetchLiveCallsError.fulfilled.match(result)) {
|
|
//success
|
|
expectTypeOf(result).toEqualTypeOf<
|
|
ReturnType<(typeof fetchLiveCallsError)['fulfilled']>
|
|
>()
|
|
|
|
expectTypeOf(result.payload).toEqualTypeOf<Item[]>()
|
|
} else {
|
|
expectTypeOf(result).toEqualTypeOf<
|
|
ReturnType<(typeof fetchLiveCallsError)['rejected']>
|
|
>()
|
|
|
|
if (result.payload) {
|
|
// rejected with value
|
|
expectTypeOf(result.payload).toEqualTypeOf<ErrorFromServer>()
|
|
} else {
|
|
// rejected by throw
|
|
expectTypeOf(result.payload).toBeUndefined()
|
|
|
|
expectTypeOf(result.error).toEqualTypeOf<SerializedError>()
|
|
|
|
expectTypeOf(result.error).not.toBeAny()
|
|
}
|
|
}
|
|
defaultDispatch(fetchLiveCallsError('asd'))
|
|
.then((result) => {
|
|
expectTypeOf(result.payload).toEqualTypeOf<
|
|
Item[] | ErrorFromServer | undefined
|
|
>()
|
|
|
|
return result
|
|
})
|
|
.then(unwrapResult)
|
|
.then((unwrapped) => {
|
|
expectTypeOf(unwrapped).toEqualTypeOf<Item[]>()
|
|
|
|
expectTypeOf(unwrapResult).parameter(0).not.toMatchTypeOf(unwrapped)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('payloadCreator first argument type has impact on asyncThunk argument', () => {
|
|
test('asyncThunk has no argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', () => 0)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<() => any>()
|
|
|
|
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<
|
|
[undefined?, AsyncThunkDispatchConfig?]
|
|
>()
|
|
|
|
expectTypeOf(asyncThunk).returns.toBeFunction()
|
|
})
|
|
|
|
test('one argument, specified as undefined: asyncThunk has no argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: undefined) => 0)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<() => any>()
|
|
|
|
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<
|
|
[undefined?, AsyncThunkDispatchConfig?]
|
|
>()
|
|
})
|
|
|
|
test('one argument, specified as void: asyncThunk has no argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: void) => 0)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<() => any>()
|
|
})
|
|
|
|
test('one argument, specified as optional number: asyncThunk has optional number argument', () => {
|
|
// this test will fail with strictNullChecks: false, that is to be expected
|
|
// in that case, we have to forbid this behavior or it will make arguments optional everywhere
|
|
const asyncThunk = createAsyncThunk('test', (arg?: number) => 0)
|
|
|
|
// Per https://github.com/reduxjs/redux-toolkit/issues/3758#issuecomment-1742152774 , this is a bug in
|
|
// TS 5.1 and 5.2, that is fixed in 5.3. Conditionally run the TS assertion here.
|
|
type IsTS51Or52 = TSVersion.Major extends 5
|
|
? TSVersion.Minor extends 1 | 2
|
|
? true
|
|
: false
|
|
: false
|
|
|
|
type expectedType = IsTS51Or52 extends true
|
|
? (arg: number) => any
|
|
: (arg?: number) => any
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<expectedType>()
|
|
|
|
// We _should_ be able to call this with no arguments, but we run into that error in 5.1 and 5.2.
|
|
// Disabling this for now.
|
|
// asyncThunk()
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameters.not.toMatchTypeOf<[string]>()
|
|
})
|
|
|
|
test('one argument, specified as number|undefined: asyncThunk has optional number argument', () => {
|
|
// this test will fail with strictNullChecks: false, that is to be expected
|
|
// in that case, we have to forbid this behavior or it will make arguments optional everywhere
|
|
const asyncThunk = createAsyncThunk(
|
|
'test',
|
|
(arg: number | undefined) => 0,
|
|
)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<(arg?: number) => any>()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(undefined)
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameters.not.toMatchTypeOf<[string]>()
|
|
})
|
|
|
|
test('one argument, specified as number|void: asyncThunk has optional number argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: number | void) => 0)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<(arg?: number) => any>()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(undefined)
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameters.not.toMatchTypeOf<[string]>()
|
|
})
|
|
|
|
test('one argument, specified as any: asyncThunk has required any argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: any) => 0)
|
|
|
|
expectTypeOf(asyncThunk).parameter(0).toBeAny()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameters.not.toMatchTypeOf<[]>()
|
|
})
|
|
|
|
test('one argument, specified as unknown: asyncThunk has required unknown argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: unknown) => 0)
|
|
|
|
expectTypeOf(asyncThunk).parameter(0).toBeUnknown()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameters.not.toMatchTypeOf<[]>()
|
|
})
|
|
|
|
test('one argument, specified as number: asyncThunk has required number argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: number) => 0)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<(arg: number) => any>()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameters.not.toMatchTypeOf<[]>()
|
|
})
|
|
|
|
test('two arguments, first specified as undefined: asyncThunk has no argument', () => {
|
|
const asyncThunk = createAsyncThunk(
|
|
'test',
|
|
(arg: undefined, thunkApi) => 0,
|
|
)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<() => any>()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(undefined)
|
|
|
|
// cannot be called with an argument
|
|
expectTypeOf(asyncThunk).parameter(0).not.toBeAny()
|
|
|
|
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<
|
|
[undefined?, AsyncThunkDispatchConfig?]
|
|
>()
|
|
})
|
|
|
|
test('two arguments, first specified as void: asyncThunk has no argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: void, thunkApi) => 0)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<() => any>()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith()
|
|
|
|
expectTypeOf(asyncThunk).parameter(0).toBeVoid()
|
|
|
|
// cannot be called with an argument
|
|
expectTypeOf(asyncThunk).parameter(0).not.toBeAny()
|
|
|
|
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<
|
|
[undefined?, AsyncThunkDispatchConfig?]
|
|
>()
|
|
})
|
|
|
|
test('two arguments, first specified as number|undefined: asyncThunk has optional number argument', () => {
|
|
// this test will fail with strictNullChecks: false, that is to be expected
|
|
// in that case, we have to forbid this behavior or it will make arguments optional everywhere
|
|
const asyncThunk = createAsyncThunk(
|
|
'test',
|
|
(arg: number | undefined, thunkApi) => 0,
|
|
)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<(arg?: number) => any>()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(undefined)
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameter(0).not.toBeString()
|
|
})
|
|
|
|
test('two arguments, first specified as number|void: asyncThunk has optional number argument', () => {
|
|
const asyncThunk = createAsyncThunk(
|
|
'test',
|
|
(arg: number | void, thunkApi) => 0,
|
|
)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<(arg?: number) => any>()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(undefined)
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameter(0).not.toBeString()
|
|
})
|
|
|
|
test('two arguments, first specified as any: asyncThunk has required any argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: any, thunkApi) => 0)
|
|
|
|
expectTypeOf(asyncThunk).parameter(0).toBeAny()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameters.not.toMatchTypeOf<[]>()
|
|
})
|
|
|
|
test('two arguments, first specified as unknown: asyncThunk has required unknown argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: unknown, thunkApi) => 0)
|
|
|
|
expectTypeOf(asyncThunk).parameter(0).toBeUnknown()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameters.not.toMatchTypeOf<[]>()
|
|
})
|
|
|
|
test('two arguments, first specified as number: asyncThunk has required number argument', () => {
|
|
const asyncThunk = createAsyncThunk('test', (arg: number, thunkApi) => 0)
|
|
|
|
expectTypeOf(asyncThunk).toMatchTypeOf<(arg: number) => any>()
|
|
|
|
expectTypeOf(asyncThunk).parameter(0).toBeNumber()
|
|
|
|
expectTypeOf(asyncThunk).toBeCallableWith(5)
|
|
|
|
expectTypeOf(asyncThunk).parameters.not.toMatchTypeOf<[]>()
|
|
})
|
|
})
|
|
|
|
test('createAsyncThunk without generics', () => {
|
|
const thunk = createAsyncThunk('test', () => {
|
|
return 'ret' as const
|
|
})
|
|
|
|
expectTypeOf(thunk).toEqualTypeOf<AsyncThunk<'ret', void, {}>>()
|
|
})
|
|
|
|
test('createAsyncThunk without generics, accessing `api` does not break return type', () => {
|
|
const thunk = createAsyncThunk('test', (_: void, api) => {
|
|
return 'ret' as const
|
|
})
|
|
|
|
expectTypeOf(thunk).toEqualTypeOf<AsyncThunk<'ret', void, {}>>()
|
|
})
|
|
|
|
test('createAsyncThunk rejectWithValue without generics: Expect correct return type', () => {
|
|
const asyncThunk = createAsyncThunk(
|
|
'test',
|
|
(_: void, { rejectWithValue }) => {
|
|
try {
|
|
return Promise.resolve(true)
|
|
} catch (e) {
|
|
return rejectWithValue(e)
|
|
}
|
|
},
|
|
)
|
|
|
|
defaultDispatch(asyncThunk())
|
|
.then((result) => {
|
|
if (asyncThunk.fulfilled.match(result)) {
|
|
expectTypeOf(result).toEqualTypeOf<
|
|
ReturnType<(typeof asyncThunk)['fulfilled']>
|
|
>()
|
|
|
|
expectTypeOf(result.payload).toBeBoolean()
|
|
|
|
expectTypeOf(result).not.toHaveProperty('error')
|
|
} else {
|
|
expectTypeOf(result).toEqualTypeOf<
|
|
ReturnType<(typeof asyncThunk)['rejected']>
|
|
>()
|
|
|
|
expectTypeOf(result.error).toEqualTypeOf<SerializedError>()
|
|
|
|
expectTypeOf(result.payload).toBeUnknown()
|
|
}
|
|
|
|
return result
|
|
})
|
|
.then(unwrapResult)
|
|
.then((unwrapped) => {
|
|
expectTypeOf(unwrapped).toBeBoolean()
|
|
})
|
|
})
|
|
|
|
test('createAsyncThunk with generics', () => {
|
|
type Funky = { somethingElse: 'Funky!' }
|
|
function funkySerializeError(err: any): Funky {
|
|
return { somethingElse: 'Funky!' }
|
|
}
|
|
|
|
// has to stay on one line or type tests fail in older TS versions
|
|
// prettier-ignore
|
|
// @ts-expect-error
|
|
const shouldFail = createAsyncThunk('without generics', () => {}, { serializeError: funkySerializeError })
|
|
|
|
const shouldWork = createAsyncThunk<
|
|
any,
|
|
void,
|
|
{ serializedErrorType: Funky }
|
|
>('with generics', () => {}, {
|
|
serializeError: funkySerializeError,
|
|
})
|
|
|
|
if (shouldWork.rejected.match(unknownAction)) {
|
|
expectTypeOf(unknownAction.error).toEqualTypeOf<Funky>()
|
|
}
|
|
})
|
|
|
|
test('`idGenerator` option takes no arguments, and returns a string', () => {
|
|
const returnsNumWithArgs = (foo: any) => 100
|
|
// has to stay on one line or type tests fail in older TS versions
|
|
// prettier-ignore
|
|
// @ts-expect-error
|
|
const shouldFailNumWithArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithArgs })
|
|
|
|
const returnsNumWithoutArgs = () => 100
|
|
// prettier-ignore
|
|
// @ts-expect-error
|
|
const shouldFailNumWithoutArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithoutArgs })
|
|
|
|
const returnsStrWithNumberArg = (foo: number) => 'foo'
|
|
// prettier-ignore
|
|
// @ts-expect-error
|
|
const shouldFailWrongArgs = createAsyncThunk('foo', (arg: string) => {}, { idGenerator: returnsStrWithNumberArg })
|
|
|
|
const returnsStrWithStringArg = (foo: string) => 'foo'
|
|
const shoulducceedCorrectArgs = createAsyncThunk(
|
|
'foo',
|
|
(arg: string) => {},
|
|
{
|
|
idGenerator: returnsStrWithStringArg,
|
|
},
|
|
)
|
|
|
|
const returnsStrWithoutArgs = () => 'foo'
|
|
const shouldSucceed = createAsyncThunk('foo', () => {}, {
|
|
idGenerator: returnsStrWithoutArgs,
|
|
})
|
|
})
|
|
|
|
test('fulfillWithValue should infer return value', () => {
|
|
// https://github.com/reduxjs/redux-toolkit/issues/2886
|
|
|
|
const initialState = {
|
|
loading: false,
|
|
obj: { magic: '' },
|
|
}
|
|
|
|
const getObj = createAsyncThunk(
|
|
'slice/getObj',
|
|
async (_: any, { fulfillWithValue, rejectWithValue }) => {
|
|
try {
|
|
return fulfillWithValue({ magic: 'object' })
|
|
} catch (rejected: any) {
|
|
return rejectWithValue(rejected?.response?.error || rejected)
|
|
}
|
|
},
|
|
)
|
|
|
|
createSlice({
|
|
name: 'slice',
|
|
initialState,
|
|
reducers: {},
|
|
extraReducers: (builder) => {
|
|
builder.addCase(getObj.fulfilled, (state, action) => {
|
|
expectTypeOf(action.payload).toEqualTypeOf<{ magic: string }>()
|
|
})
|
|
},
|
|
})
|
|
})
|
|
|
|
test('meta return values', () => {
|
|
// return values
|
|
createAsyncThunk<'ret', void, {}>('test', (_, api) => 'ret' as const)
|
|
createAsyncThunk<'ret', void, {}>('test', async (_, api) => 'ret' as const)
|
|
createAsyncThunk<'ret', void, { fulfilledMeta: string }>('test', (_, api) =>
|
|
api.fulfillWithValue('ret' as const, ''),
|
|
)
|
|
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
|
'test',
|
|
async (_, api) => api.fulfillWithValue('ret' as const, ''),
|
|
)
|
|
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
|
'test',
|
|
// @ts-expect-error has to be a fulfilledWithValue call
|
|
(_, api) => 'ret' as const,
|
|
)
|
|
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
|
'test',
|
|
// @ts-expect-error has to be a fulfilledWithValue call
|
|
async (_, api) => 'ret' as const,
|
|
)
|
|
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
|
'test', // @ts-expect-error should only allow returning with 'test'
|
|
(_, api) => api.fulfillWithValue(5, ''),
|
|
)
|
|
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
|
'test', // @ts-expect-error should only allow returning with 'test'
|
|
async (_, api) => api.fulfillWithValue(5, ''),
|
|
)
|
|
|
|
// reject values
|
|
createAsyncThunk<'ret', void, { rejectValue: string }>('test', (_, api) =>
|
|
api.rejectWithValue('ret'),
|
|
)
|
|
createAsyncThunk<'ret', void, { rejectValue: string }>(
|
|
'test',
|
|
async (_, api) => api.rejectWithValue('ret'),
|
|
)
|
|
createAsyncThunk<
|
|
'ret',
|
|
void,
|
|
{ rejectValue: string; rejectedMeta: number }
|
|
>('test', (_, api) => api.rejectWithValue('ret', 5))
|
|
createAsyncThunk<
|
|
'ret',
|
|
void,
|
|
{ rejectValue: string; rejectedMeta: number }
|
|
>('test', async (_, api) => api.rejectWithValue('ret', 5))
|
|
createAsyncThunk<
|
|
'ret',
|
|
void,
|
|
{ rejectValue: string; rejectedMeta: number }
|
|
>('test', (_, api) => api.rejectWithValue('ret', 5))
|
|
createAsyncThunk<
|
|
'ret',
|
|
void,
|
|
{ rejectValue: string; rejectedMeta: number }
|
|
>(
|
|
'test',
|
|
// @ts-expect-error wrong rejectedMeta type
|
|
(_, api) => api.rejectWithValue('ret', ''),
|
|
)
|
|
createAsyncThunk<
|
|
'ret',
|
|
void,
|
|
{ rejectValue: string; rejectedMeta: number }
|
|
>(
|
|
'test',
|
|
// @ts-expect-error wrong rejectedMeta type
|
|
async (_, api) => api.rejectWithValue('ret', ''),
|
|
)
|
|
createAsyncThunk<
|
|
'ret',
|
|
void,
|
|
{ rejectValue: string; rejectedMeta: number }
|
|
>(
|
|
'test',
|
|
// @ts-expect-error wrong rejectValue type
|
|
(_, api) => api.rejectWithValue(5, ''),
|
|
)
|
|
createAsyncThunk<
|
|
'ret',
|
|
void,
|
|
{ rejectValue: string; rejectedMeta: number }
|
|
>(
|
|
'test',
|
|
// @ts-expect-error wrong rejectValue type
|
|
async (_, api) => api.rejectWithValue(5, ''),
|
|
)
|
|
})
|
|
|
|
test('usage with config override generic', () => {
|
|
const typedCAT = createAsyncThunk.withTypes<{
|
|
state: RootState
|
|
dispatch: AppDispatch
|
|
rejectValue: string
|
|
extra: { s: string; n: number }
|
|
}>()
|
|
|
|
// inferred usage
|
|
const thunk = typedCAT('foo', (arg: number, api) => {
|
|
// correct getState Type
|
|
const test1: number = api.getState().foo.value
|
|
// correct dispatch type
|
|
const test2: number = api.dispatch((dispatch, getState) => {
|
|
expectTypeOf(dispatch).toEqualTypeOf<
|
|
ThunkDispatch<{ foo: { value: number } }, undefined, UnknownAction>
|
|
>()
|
|
|
|
expectTypeOf(getState).toEqualTypeOf<() => { foo: { value: number } }>()
|
|
|
|
return getState().foo.value
|
|
})
|
|
|
|
// correct extra type
|
|
const { s, n } = api.extra
|
|
|
|
expectTypeOf(s).toBeString()
|
|
|
|
expectTypeOf(n).toBeNumber()
|
|
|
|
if (1 < 2)
|
|
// @ts-expect-error
|
|
return api.rejectWithValue(5)
|
|
if (1 < 2) return api.rejectWithValue('test')
|
|
return test1 + test2
|
|
})
|
|
|
|
// usage with two generics
|
|
const thunk2 = typedCAT<number, string>('foo', (arg, api) => {
|
|
expectTypeOf(arg).toBeString()
|
|
|
|
// correct getState Type
|
|
const test1: number = api.getState().foo.value
|
|
// correct dispatch type
|
|
const test2: number = api.dispatch((dispatch, getState) => {
|
|
expectTypeOf(dispatch).toEqualTypeOf<
|
|
ThunkDispatch<{ foo: { value: number } }, undefined, UnknownAction>
|
|
>()
|
|
|
|
expectTypeOf(getState).toEqualTypeOf<() => { foo: { value: number } }>()
|
|
|
|
return getState().foo.value
|
|
})
|
|
// correct extra type
|
|
const { s, n } = api.extra
|
|
|
|
expectTypeOf(s).toBeString()
|
|
|
|
expectTypeOf(n).toBeNumber()
|
|
|
|
if (1 < 2) expectTypeOf(api.rejectWithValue).toBeCallableWith('test')
|
|
|
|
expectTypeOf(api.rejectWithValue).parameter(0).not.toBeNumber()
|
|
|
|
expectTypeOf(api.rejectWithValue).parameters.toEqualTypeOf<[string]>()
|
|
|
|
return api.rejectWithValue('test')
|
|
})
|
|
|
|
// usage with config override generic
|
|
const thunk3 = typedCAT<number, string, { rejectValue: number }>(
|
|
'foo',
|
|
(arg, api) => {
|
|
expectTypeOf(arg).toBeString()
|
|
|
|
// correct getState Type
|
|
const test1: number = api.getState().foo.value
|
|
// correct dispatch type
|
|
const test2: number = api.dispatch((dispatch, getState) => {
|
|
expectTypeOf(dispatch).toEqualTypeOf<
|
|
ThunkDispatch<{ foo: { value: number } }, undefined, UnknownAction>
|
|
>()
|
|
|
|
expectTypeOf(getState).toEqualTypeOf<
|
|
() => { foo: { value: number } }
|
|
>()
|
|
|
|
return getState().foo.value
|
|
})
|
|
// correct extra type
|
|
const { s, n } = api.extra
|
|
|
|
expectTypeOf(s).toBeString()
|
|
|
|
expectTypeOf(n).toBeNumber()
|
|
|
|
if (1 < 2) return api.rejectWithValue(5)
|
|
if (1 < 2) expectTypeOf(api.rejectWithValue).toBeCallableWith(5)
|
|
|
|
expectTypeOf(api.rejectWithValue).parameter(0).not.toBeString()
|
|
|
|
expectTypeOf(api.rejectWithValue).parameters.toEqualTypeOf<[number]>()
|
|
|
|
return api.rejectWithValue(5)
|
|
},
|
|
)
|
|
|
|
const slice = createSlice({
|
|
name: 'foo',
|
|
initialState: { value: 0 },
|
|
reducers: {},
|
|
extraReducers(builder) {
|
|
builder
|
|
.addCase(thunk.fulfilled, (state, action) => {
|
|
state.value += action.payload
|
|
})
|
|
.addCase(thunk.rejected, (state, action) => {
|
|
expectTypeOf(action.payload).toEqualTypeOf<string | undefined>()
|
|
})
|
|
.addCase(thunk2.fulfilled, (state, action) => {
|
|
state.value += action.payload
|
|
})
|
|
.addCase(thunk2.rejected, (state, action) => {
|
|
expectTypeOf(action.payload).toEqualTypeOf<string | undefined>()
|
|
})
|
|
.addCase(thunk3.fulfilled, (state, action) => {
|
|
state.value += action.payload
|
|
})
|
|
.addCase(thunk3.rejected, (state, action) => {
|
|
expectTypeOf(action.payload).toEqualTypeOf<number | undefined>()
|
|
})
|
|
},
|
|
})
|
|
|
|
const store = configureStore({
|
|
reducer: {
|
|
foo: slice.reducer,
|
|
},
|
|
})
|
|
|
|
type RootState = ReturnType<typeof store.getState>
|
|
type AppDispatch = typeof store.dispatch
|
|
})
|
|
|
|
test('rejectedMeta', async () => {
|
|
const getNewStore = () =>
|
|
configureStore({
|
|
reducer(actions = [], action) {
|
|
return [...actions, action]
|
|
},
|
|
})
|
|
|
|
const store = getNewStore()
|
|
|
|
const fulfilledThunk = createAsyncThunk<
|
|
string,
|
|
string,
|
|
{ rejectedMeta: { extraProp: string } }
|
|
>('test', (arg: string, { rejectWithValue }) => {
|
|
return rejectWithValue('damn!', { extraProp: 'baz' })
|
|
})
|
|
|
|
const promise = store.dispatch(fulfilledThunk('testArg'))
|
|
|
|
const ret = await promise
|
|
|
|
if (ret.meta.requestStatus === 'rejected' && ret.meta.rejectedWithValue) {
|
|
expectTypeOf(ret.meta.extraProp).toBeString()
|
|
} else {
|
|
// could be caused by a `throw`, `abort()` or `condition` - no `rejectedMeta` in that case
|
|
expectTypeOf(ret.meta).not.toHaveProperty('extraProp')
|
|
}
|
|
})
|
|
})
|