Skip to content

Commit d13d26a

Browse files
authored
createAsyncThunk return fulfilled/rejected action instead of re-… (#361)
* createAsyncThunk return fulfilled/rejected action instead of re-trowing errors * add unwrapResult helper
1 parent fbba32d commit d13d26a

File tree

3 files changed

+81
-24
lines changed

3 files changed

+81
-24
lines changed

etc/redux-toolkit.api.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,13 @@ export function createAction<P = void, T extends string = string>(type: T): Payl
102102
export function createAction<PA extends PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;
103103

104104
// @alpha (undocumented)
105-
export function createAsyncThunk<ActionType extends string, Returned, ActionParams = void, TA extends AsyncThunksArgs<any, any, any> = AsyncThunksArgs<unknown, unknown, Dispatch>>(type: ActionType, payloadCreator: (args: ActionParams, thunkArgs: TA) => Promise<Returned> | Returned): {
106-
(args: ActionParams): (dispatch: TA["dispatch"], getState: TA["getState"], extra: TA["extra"]) => Promise<any>;
105+
export function createAsyncThunk<ActionType extends string, Returned, ActionParams = void, TA extends AsyncThunksArgs<any, any, any> = AsyncThunksArgs<unknown, unknown, Dispatch>>(type: ActionType, payloadCreator: (args: ActionParams, thunkArgs: TA) => Promise<Returned> | Returned): ((args: ActionParams) => (dispatch: TA["dispatch"], getState: TA["getState"], extra: TA["extra"]) => Promise<import("./createAction").PayloadAction<Returned, string, {
106+
args: ActionParams;
107+
requestId: string;
108+
}, never> | import("./createAction").PayloadAction<undefined, string, {
109+
args: ActionParams;
110+
requestId: string;
111+
}, Error>>) & {
107112
pending: import("./createAction").ActionCreatorWithPreparedPayload<[string, ActionParams], undefined, string, never, {
108113
args: ActionParams;
109114
requestId: string;
@@ -116,6 +121,13 @@ export function createAsyncThunk<ActionType extends string, Returned, ActionPara
116121
args: ActionParams;
117122
requestId: string;
118123
}>;
124+
unwrapResult: (returned: import("./createAction").PayloadAction<Returned, string, {
125+
args: ActionParams;
126+
requestId: string;
127+
}, never> | import("./createAction").PayloadAction<undefined, string, {
128+
args: ActionParams;
129+
requestId: string;
130+
}, Error>) => Returned;
119131
};
120132

121133
// @alpha (undocumented)

src/createAsyncThunk.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Dispatch } from 'redux'
22
import nanoid from 'nanoid'
33
import { createAction } from './createAction'
44

5+
type Await<P> = P extends PromiseLike<infer T> ? T : P
6+
57
type AsyncThunksArgs<S, E, D extends Dispatch = Dispatch> = {
68
dispatch: D
79
getState: S
@@ -101,34 +103,47 @@ export function createAsyncThunk<
101103
) => {
102104
const requestId = nanoid()
103105

104-
let result: Returned
106+
let finalAction: ReturnType<typeof fulfilled | typeof rejected>
105107
try {
106108
dispatch(pending(requestId, args))
107109

108-
result = (await payloadCreator(args, {
109-
dispatch,
110-
getState,
111-
extra,
112-
requestId
113-
} as TA)) as Returned
110+
finalAction = fulfilled(
111+
await payloadCreator(args, {
112+
dispatch,
113+
getState,
114+
extra,
115+
requestId
116+
} as TA),
117+
requestId,
118+
args
119+
)
114120
} catch (err) {
115121
const serializedError = miniSerializeError(err)
116-
dispatch(rejected(serializedError, requestId, args))
117-
// Rethrow this so the user can handle if desired
118-
throw err
122+
finalAction = rejected(serializedError, requestId, args)
119123
}
120124

121125
// We dispatch "success" _after_ the catch, to avoid having any errors
122126
// here get swallowed by the try/catch block,
123127
// per https://twitter.com/dan_abramov/status/770914221638942720
124128
// and https://redux-toolkit.js.org/tutorials/advanced-tutorial#async-error-handling-logic-in-thunks
125-
return dispatch(fulfilled(result!, requestId, args))
129+
dispatch(finalAction)
130+
return finalAction
126131
}
127132
}
128133

129-
actionCreator.pending = pending
130-
actionCreator.rejected = rejected
131-
actionCreator.fulfilled = fulfilled
134+
function unwrapResult(
135+
returned: Await<ReturnType<ReturnType<typeof actionCreator>>>
136+
) {
137+
if (rejected.match(returned)) {
138+
throw returned.error
139+
}
140+
return returned.payload
141+
}
132142

133-
return actionCreator
143+
return Object.assign(actionCreator, {
144+
pending,
145+
rejected,
146+
fulfilled,
147+
unwrapResult
148+
})
134149
}
Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,56 @@
1-
import { createAsyncThunk, Dispatch, createReducer } from 'src'
1+
import { createAsyncThunk, Dispatch, createReducer, AnyAction } from 'src'
22
import { ThunkDispatch } from 'redux-thunk'
3+
import { promises } from 'fs'
34

45
function expectType<T>(t: T) {
56
return t
67
}
78
function fn() {}
89

910
// basic usage
10-
{
11-
const dispatch = fn as ThunkDispatch<any, any, any>
11+
;(async function() {
12+
const dispatch = fn as ThunkDispatch<{}, any, AnyAction>
1213

1314
const async = createAsyncThunk('test', (id: number) =>
1415
Promise.resolve(id * 2)
1516
)
16-
dispatch(async(3))
1717

1818
const reducer = createReducer({}, builder =>
1919
builder
20-
.addCase(async.pending, (_, action) => {})
20+
.addCase(async.pending, (_, action) => {
21+
expectType<ReturnType<typeof async['pending']>>(action)
22+
})
2123
.addCase(async.fulfilled, (_, action) => {
24+
expectType<ReturnType<typeof async['fulfilled']>>(action)
2225
expectType<number>(action.payload)
2326
})
24-
.addCase(async.rejected, (_, action) => {})
27+
.addCase(async.rejected, (_, action) => {
28+
expectType<ReturnType<typeof async['rejected']>>(action)
29+
expectType<Error>(action.error)
30+
})
2531
)
26-
}
32+
33+
const promise = dispatch(async(3))
34+
const result = await promise
35+
36+
if (async.fulfilled.match(result)) {
37+
expectType<ReturnType<typeof async['fulfilled']>>(result)
38+
// typings:expect-error
39+
expectType<ReturnType<typeof async['rejected']>>(result)
40+
} else {
41+
expectType<ReturnType<typeof async['rejected']>>(result)
42+
// typings:expect-error
43+
expectType<ReturnType<typeof async['fulfilled']>>(result)
44+
}
45+
46+
promise
47+
.then(async.unwrapResult)
48+
.then(result => {
49+
expectType<number>(result)
50+
// typings:expect-error
51+
expectType<Error>(result)
52+
})
53+
.catch(error => {
54+
// catch is always any-typed, nothing we can do here
55+
})
56+
})()

0 commit comments

Comments
 (0)