Skip to content

Commit 74a0249

Browse files
authored
payloadCreator arg argument => asyncThunk arg type (#502)
* payloadCreator arg argument => asyncThunk arg type * allow `|void` typing for optional argument, handle `|undefined` for strictNullChecks: false * fix tests * extract types, export `AsyncThunkAction` type
1 parent b1f3bb0 commit 74a0249

File tree

4 files changed

+248
-23
lines changed

4 files changed

+248
-23
lines changed

etc/redux-toolkit.api.md

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export interface ActionReducerMapBuilder<State> {
5959
// @public @deprecated
6060
export type Actions<T extends keyof any = string> = Record<T, Action>;
6161

62+
// @public
63+
export type AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig> = (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<AsyncThunkReturnValue<ThunkArg, Returned, GetRejectValue<ThunkApiConfig>>> & {
64+
abort(reason?: string): void;
65+
};
66+
6267
// @public
6368
export type AsyncThunkPayloadCreator<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}> = (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => AsyncThunkPayloadCreatorReturnValue<Returned, ThunkApiConfig>;
6469

@@ -111,16 +116,7 @@ export function createAction<P = void, T extends string = string>(type: T): Payl
111116
export function createAction<PA extends PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;
112117

113118
// @public (undocumented)
114-
export function createAsyncThunk<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(type: string, payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>): ((arg: ThunkArg) => (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<PayloadAction<Returned, string, {
115-
arg: ThunkArg;
116-
requestId: string;
117-
}, never> | PayloadAction<GetRejectValue<ThunkApiConfig> | undefined, string, {
118-
arg: ThunkArg;
119-
requestId: string;
120-
aborted: boolean;
121-
}, SerializedError>> & {
122-
abort: (reason?: string | undefined) => void;
123-
}) & {
119+
export function createAsyncThunk<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(type: string, payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>): IsAny<ThunkArg, (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>, unknown extends ThunkArg ? (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> : [ThunkArg] extends [void] | [undefined] ? () => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> : [void] extends [ThunkArg] ? (arg?: ThunkArg | undefined) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> : [undefined] extends [ThunkArg] ? (arg?: ThunkArg | undefined) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> : (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>> & {
124120
pending: ActionCreatorWithPreparedPayload<[string, ThunkArg], undefined, string, never, {
125121
arg: ThunkArg;
126122
requestId: string;

src/createAsyncThunk.ts

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ActionCreatorWithPreparedPayload
66
} from './createAction'
77
import { ThunkDispatch } from 'redux-thunk'
8-
import { FallbackIfUnknown } from './tsHelpers'
8+
import { FallbackIfUnknown, IsAny } from './tsHelpers'
99
import { nanoid } from './nanoid'
1010

1111
// @ts-ignore we need the import of these types due to a bundling issue.
@@ -130,6 +130,64 @@ export type AsyncThunkPayloadCreator<
130130
thunkAPI: GetThunkAPI<ThunkApiConfig>
131131
) => AsyncThunkPayloadCreatorReturnValue<Returned, ThunkApiConfig>
132132

133+
type AsyncThunkReturnValue<ThunkArg, FulfilledValue, RejectedValue> =
134+
| PayloadAction<FulfilledValue, string, { arg: ThunkArg; requestId: string }>
135+
| PayloadAction<
136+
undefined | RejectedValue,
137+
string,
138+
{ arg: ThunkArg; requestId: string; aborted: boolean },
139+
SerializedError
140+
>
141+
/**
142+
* A ThunkAction created by `createAsyncThunk`.
143+
* Dispatching it returns a Promise for either a
144+
* fulfilled or rejected action.
145+
* Also, the returned value contains a `abort()` method
146+
* that allows the asyncAction to be cancelled from the outside.
147+
*
148+
* @public
149+
*/
150+
export type AsyncThunkAction<
151+
Returned,
152+
ThunkArg,
153+
ThunkApiConfig extends AsyncThunkConfig
154+
> = (
155+
dispatch: GetDispatch<ThunkApiConfig>,
156+
getState: () => GetState<ThunkApiConfig>,
157+
extra: GetExtra<ThunkApiConfig>
158+
) => Promise<
159+
AsyncThunkReturnValue<ThunkArg, Returned, GetRejectValue<ThunkApiConfig>>
160+
> & {
161+
abort(reason?: string): void
162+
}
163+
164+
type AsyncThunkActionCreator<
165+
Returned,
166+
ThunkArg,
167+
ThunkApiConfig extends AsyncThunkConfig
168+
> = IsAny<
169+
ThunkArg,
170+
// any handling
171+
(arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>,
172+
// unknown handling
173+
unknown extends ThunkArg
174+
? (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument not specified or specified as void or undefined
175+
: [ThunkArg] extends [void] | [undefined]
176+
? () => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument contains void
177+
: [void] extends [ThunkArg] // make optional
178+
? (arg?: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument contains undefined
179+
: [undefined] extends [ThunkArg]
180+
? WithStrictNullChecks<
181+
// with strict nullChecks: make optional
182+
(
183+
arg?: ThunkArg
184+
) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>,
185+
// without strict null checks this will match everything, so don't make it optional
186+
(arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
187+
> // default case: normal argument
188+
: (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
189+
>
190+
133191
/**
134192
*
135193
* @param type
@@ -216,12 +274,10 @@ If you want to use the AbortController to react to \`abort\` events, please cons
216274
}
217275
}
218276

219-
function actionCreator(arg: ThunkArg) {
220-
return (
221-
dispatch: GetDispatch<ThunkApiConfig>,
222-
getState: () => GetState<ThunkApiConfig>,
223-
extra: GetExtra<ThunkApiConfig>
224-
) => {
277+
function actionCreator(
278+
arg: ThunkArg
279+
): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> {
280+
return (dispatch, getState, extra) => {
225281
const requestId = nanoid()
226282

227283
const abortController = new AC()
@@ -277,11 +333,18 @@ If you want to use the AbortController to react to \`abort\` events, please cons
277333
}
278334
}
279335

280-
return Object.assign(actionCreator, {
281-
pending,
282-
rejected,
283-
fulfilled
284-
})
336+
return Object.assign(
337+
actionCreator as AsyncThunkActionCreator<
338+
Returned,
339+
ThunkArg,
340+
ThunkApiConfig
341+
>,
342+
{
343+
pending,
344+
rejected,
345+
fulfilled
346+
}
347+
)
285348
}
286349

287350
type ActionTypesWithOptionalErrorAction =
@@ -304,3 +367,7 @@ export function unwrapResult<R extends ActionTypesWithOptionalErrorAction>(
304367
}
305368
return (returned as any).payload
306369
}
370+
371+
type WithStrictNullChecks<True, False> = undefined extends boolean
372+
? False
373+
: True

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export {
9696
} from './entities/models'
9797

9898
export {
99+
AsyncThunkAction,
99100
AsyncThunkPayloadCreatorReturnValue,
100101
AsyncThunkPayloadCreator,
101102
createAsyncThunk,

type-tests/files/createAsyncThunk.typetest.ts

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ThunkDispatch } from 'redux-thunk'
33
import { unwrapResult, SerializedError } from 'src/createAsyncThunk'
44

55
import apiRequest, { AxiosError } from 'axios'
6-
import { IsAny } from 'src/tsHelpers'
6+
import { IsAny, IsUnknown } from 'src/tsHelpers'
77

88
function expectType<T>(t: T) {
99
return t
@@ -181,3 +181,164 @@ const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
181181
}
182182
})
183183
}
184+
185+
/**
186+
* payloadCreator first argument type has impact on asyncThunk argument
187+
*/
188+
{
189+
// no argument: asyncThunk has no argument
190+
{
191+
const asyncThunk = createAsyncThunk('test', () => 0)
192+
expectType<() => any>(asyncThunk)
193+
// typings:expect-error cannot be called with an argument
194+
asyncThunk(0 as any)
195+
}
196+
197+
// one argument, specified as undefined: asyncThunk has no argument
198+
{
199+
const asyncThunk = createAsyncThunk('test', (arg: undefined) => 0)
200+
expectType<() => any>(asyncThunk)
201+
// typings:expect-error cannot be called with an argument
202+
asyncThunk(0 as any)
203+
}
204+
205+
// one argument, specified as void: asyncThunk has no argument
206+
{
207+
const asyncThunk = createAsyncThunk('test', (arg: void) => 0)
208+
expectType<() => any>(asyncThunk)
209+
// typings:expect-error cannot be called with an argument
210+
asyncThunk(0 as any)
211+
}
212+
213+
// one argument, specified as optional number: asyncThunk has optional number argument
214+
// this test will fail with strictNullChecks: false, that is to be expected
215+
// in that case, we have to forbid this behaviour or it will make arguments optional everywhere
216+
{
217+
const asyncThunk = createAsyncThunk('test', (arg?: number) => 0)
218+
expectType<(arg?: number) => any>(asyncThunk)
219+
asyncThunk()
220+
asyncThunk(5)
221+
// typings:expect-error
222+
asyncThunk('string')
223+
}
224+
225+
// one argument, specified as number|undefined: asyncThunk has optional number argument
226+
// this test will fail with strictNullChecks: false, that is to be expected
227+
// in that case, we have to forbid this behaviour or it will make arguments optional everywhere
228+
{
229+
const asyncThunk = createAsyncThunk('test', (arg: number | undefined) => 0)
230+
expectType<(arg?: number) => any>(asyncThunk)
231+
asyncThunk()
232+
asyncThunk(5)
233+
// typings:expect-error
234+
asyncThunk('string')
235+
}
236+
237+
// one argument, specified as number|void: asyncThunk has optional number argument
238+
{
239+
const asyncThunk = createAsyncThunk('test', (arg: number | void) => 0)
240+
expectType<(arg?: number) => any>(asyncThunk)
241+
asyncThunk()
242+
asyncThunk(5)
243+
// typings:expect-error
244+
asyncThunk('string')
245+
}
246+
247+
// one argument, specified as any: asyncThunk has required any argument
248+
{
249+
const asyncThunk = createAsyncThunk('test', (arg: any) => 0)
250+
expectType<IsAny<Parameters<typeof asyncThunk>[0], true, false>>(true)
251+
asyncThunk(5)
252+
// typings:expect-error
253+
asyncThunk()
254+
}
255+
256+
// one argument, specified as unknown: asyncThunk has required unknown argument
257+
{
258+
const asyncThunk = createAsyncThunk('test', (arg: unknown) => 0)
259+
expectType<IsUnknown<Parameters<typeof asyncThunk>[0], true, false>>(true)
260+
asyncThunk(5)
261+
// typings:expect-error
262+
asyncThunk()
263+
}
264+
265+
// one argument, specified as number: asyncThunk has required number argument
266+
{
267+
const asyncThunk = createAsyncThunk('test', (arg: number) => 0)
268+
expectType<(arg: number) => any>(asyncThunk)
269+
asyncThunk(5)
270+
// typings:expect-error
271+
asyncThunk()
272+
}
273+
274+
// two arguments, first specified as undefined: asyncThunk has no argument
275+
{
276+
const asyncThunk = createAsyncThunk('test', (arg: undefined, thunkApi) => 0)
277+
expectType<() => any>(asyncThunk)
278+
// typings:expect-error cannot be called with an argument
279+
asyncThunk(0 as any)
280+
}
281+
282+
// two arguments, first specified as void: asyncThunk has no argument
283+
{
284+
const asyncThunk = createAsyncThunk('test', (arg: void, thunkApi) => 0)
285+
expectType<() => any>(asyncThunk)
286+
// typings:expect-error cannot be called with an argument
287+
asyncThunk(0 as any)
288+
}
289+
290+
// two arguments, first specified as number|undefined: asyncThunk has optional number argument
291+
// this test will fail with strictNullChecks: false, that is to be expected
292+
// in that case, we have to forbid this behaviour or it will make arguments optional everywhere
293+
{
294+
const asyncThunk = createAsyncThunk(
295+
'test',
296+
(arg: number | undefined, thunkApi) => 0
297+
)
298+
expectType<(arg?: number) => any>(asyncThunk)
299+
asyncThunk()
300+
asyncThunk(5)
301+
// typings:expect-error
302+
asyncThunk('string')
303+
}
304+
305+
// two arguments, first specified as number|void: asyncThunk has optional number argument
306+
{
307+
const asyncThunk = createAsyncThunk(
308+
'test',
309+
(arg: number | void, thunkApi) => 0
310+
)
311+
expectType<(arg?: number) => any>(asyncThunk)
312+
asyncThunk()
313+
asyncThunk(5)
314+
// typings:expect-error
315+
asyncThunk('string')
316+
}
317+
318+
// two arguments, first specified as any: asyncThunk has required any argument
319+
{
320+
const asyncThunk = createAsyncThunk('test', (arg: any, thunkApi) => 0)
321+
expectType<IsAny<Parameters<typeof asyncThunk>[0], true, false>>(true)
322+
asyncThunk(5)
323+
// typings:expect-error
324+
asyncThunk()
325+
}
326+
327+
// two arguments, first specified as unknown: asyncThunk has required unknown argument
328+
{
329+
const asyncThunk = createAsyncThunk('test', (arg: unknown, thunkApi) => 0)
330+
expectType<IsUnknown<Parameters<typeof asyncThunk>[0], true, false>>(true)
331+
asyncThunk(5)
332+
// typings:expect-error
333+
asyncThunk()
334+
}
335+
336+
// two arguments, first specified as number: asyncThunk has required number argument
337+
{
338+
const asyncThunk = createAsyncThunk('test', (arg: number, thunkApi) => 0)
339+
expectType<(arg: number) => any>(asyncThunk)
340+
asyncThunk(5)
341+
// typings:expect-error
342+
asyncThunk()
343+
}
344+
}

0 commit comments

Comments
 (0)