Skip to content

Commit 44a5501

Browse files
committed
add experimental createAsyncThunk.rejectWithValue
1 parent a6e5e5f commit 44a5501

File tree

2 files changed

+72
-7
lines changed

2 files changed

+72
-7
lines changed

src/createAsyncThunk.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,30 @@ export interface SerializedError {
2929
code?: string
3030
}
3131

32-
const commonProperties: (keyof SerializedError)[] = [
32+
const commonProperties: Array<keyof SerializedError> = [
3333
'name',
3434
'message',
3535
'stack',
3636
'code'
3737
]
3838

39+
const rejectionSymbol: unique symbol = Symbol('rejectWithValue')
40+
type RejectWithValue<RejectValue> = {
41+
[rejectionSymbol]: 'reject'
42+
value: RejectValue
43+
}
44+
function rejectWithValue<RejectValue>(
45+
value: RejectValue
46+
): RejectWithValue<RejectValue> {
47+
return {
48+
[rejectionSymbol]: 'reject',
49+
value
50+
}
51+
}
52+
function isRejectWithValue(value: any): value is RejectWithValue<unknown> {
53+
return value[rejectionSymbol] === 'reject'
54+
}
55+
3956
// Reworked from https://github.com/sindresorhus/serialize-error
4057
export const miniSerializeError = (value: any): any => {
4158
if (typeof value === 'object' && value !== null) {
@@ -56,6 +73,7 @@ type AsyncThunkConfig = {
5673
state?: unknown
5774
dispatch?: Dispatch
5875
extra?: unknown
76+
rejectValue?: unknown
5977
}
6078

6179
type GetState<ThunkApiConfig> = ThunkApiConfig extends {
@@ -84,6 +102,11 @@ type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
84102
GetExtra<ThunkApiConfig>,
85103
GetDispatch<ThunkApiConfig>
86104
>
105+
type GetRejectValue<ThunkApiConfig> = ThunkApiConfig extends {
106+
rejectValue: infer RejectValue
107+
}
108+
? RejectValue
109+
: undefined
87110

88111
/**
89112
*
@@ -101,8 +124,14 @@ export function createAsyncThunk<
101124
payloadCreator: (
102125
arg: ThunkArg,
103126
thunkAPI: GetThunkAPI<ThunkApiConfig>
104-
) => Promise<Returned> | Returned
127+
) =>
128+
| Promise<Returned>
129+
| Returned
130+
| Promise<RejectWithValue<GetRejectValue<ThunkApiConfig>>>
131+
| RejectWithValue<GetRejectValue<ThunkApiConfig>>
105132
) {
133+
type RejectedValue = GetRejectValue<ThunkApiConfig>
134+
106135
const fulfilled = createAction(
107136
type + '/fulfilled',
108137
(result: Returned, requestId: string, arg: ThunkArg) => {
@@ -125,10 +154,15 @@ export function createAsyncThunk<
125154

126155
const rejected = createAction(
127156
type + '/rejected',
128-
(error: Error, requestId: string, arg: ThunkArg) => {
157+
(
158+
error: Error | null,
159+
requestId: string,
160+
arg: ThunkArg,
161+
payload?: RejectedValue
162+
) => {
129163
const aborted = error && error.name === 'AbortError'
130164
return {
131-
payload: undefined,
165+
payload,
132166
error: miniSerializeError(error),
133167
meta: {
134168
arg,
@@ -175,7 +209,12 @@ export function createAsyncThunk<
175209
requestId,
176210
signal: abortController.signal
177211
})
178-
).then(result => fulfilled(result, requestId, arg))
212+
).then(result => {
213+
if (isRejectWithValue(result)) {
214+
return rejected(null, requestId, arg, result.value)
215+
}
216+
return fulfilled(result, requestId, arg)
217+
})
179218
])
180219
} catch (err) {
181220
finalAction = rejected(err, requestId, arg)
@@ -198,6 +237,7 @@ export function createAsyncThunk<
198237
fulfilled
199238
})
200239
}
240+
createAsyncThunk.rejectWithValue = rejectWithValue
201241

202242
/**
203243
* @alpha

type-tests/files/createAsyncThunk.typetest.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createAsyncThunk, Dispatch, createReducer, AnyAction } from 'src'
22
import { ThunkDispatch } from 'redux-thunk'
3-
import { promises } from 'fs'
43
import { unwrapResult } from 'src/createAsyncThunk'
54

65
function expectType<T>(t: T) {
@@ -97,4 +96,30 @@ const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
9796
correctDispatch(fetchBooksTAC(1))
9897
// typings:expect-error
9998
defaultDispatch(fetchBooksTAC(1))
100-
})()
99+
})()(
100+
/**
101+
* returning a rejected action from the promise creator is possible
102+
*/
103+
async () => {
104+
type ReturnValue = { data: 'success' }
105+
type RejectValue = { data: 'error' }
106+
107+
const fetchBooksTAC = createAsyncThunk<
108+
ReturnValue,
109+
number,
110+
{
111+
rejectValue: RejectValue
112+
}
113+
>('books/fetch', async arg => {
114+
return createAsyncThunk.rejectWithValue<RejectValue>({ data: 'error' })
115+
})
116+
117+
const returned = await defaultDispatch(fetchBooksTAC(1))
118+
if (fetchBooksTAC.rejected.match(returned)) {
119+
expectType<undefined | RejectValue>(returned.payload)
120+
expectType<RejectValue>(returned.payload!)
121+
} else {
122+
expectType<ReturnValue>(returned.payload)
123+
}
124+
}
125+
)()

0 commit comments

Comments
 (0)