UNPKG

18.4 kBPlain TextView Raw
1/* eslint-disable no-lone-blocks */
2import type { AnyAction, SerializedError, AsyncThunk } from '@reduxjs/toolkit'
3import { createAsyncThunk, createReducer, unwrapResult } from '@reduxjs/toolkit'
4import type { ThunkDispatch } from 'redux-thunk'
5
6import type { AxiosError } from 'axios'
7import apiRequest from 'axios'
8import type { IsAny, IsUnknown } from '@internal/tsHelpers'
9import { expectType } from './helpers'
10import type {
11 AsyncThunkFulfilledActionCreator,
12 AsyncThunkRejectedActionCreator,
13} from '@internal/createAsyncThunk'
14
15const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
16const anyAction = { type: 'foo' } as AnyAction
17
18// basic usage
19;(async function () {
20 const async = createAsyncThunk('test', (id: number) =>
21 Promise.resolve(id * 2)
22 )
23
24 const reducer = createReducer({}, (builder) =>
25 builder
26 .addCase(async.pending, (_, action) => {
27 expectType<ReturnType<typeof async['pending']>>(action)
28 })
29 .addCase(async.fulfilled, (_, action) => {
30 expectType<ReturnType<typeof async['fulfilled']>>(action)
31 expectType<number>(action.payload)
32 })
33 .addCase(async.rejected, (_, action) => {
34 expectType<ReturnType<typeof async['rejected']>>(action)
35 expectType<Partial<Error> | undefined>(action.error)
36 })
37 )
38
39 const promise = defaultDispatch(async(3))
40
41 expectType<string>(promise.requestId)
42 expectType<number>(promise.arg)
43 expectType<(reason?: string) => void>(promise.abort)
44
45 const result = await promise
46
47 if (async.fulfilled.match(result)) {
48 expectType<ReturnType<typeof async['fulfilled']>>(result)
49 // @ts-expect-error
50 expectType<ReturnType<typeof async['rejected']>>(result)
51 } else {
52 expectType<ReturnType<typeof async['rejected']>>(result)
53 // @ts-expect-error
54 expectType<ReturnType<typeof async['fulfilled']>>(result)
55 }
56
57 promise
58 .then(unwrapResult)
59 .then((result) => {
60 expectType<number>(result)
61 // @ts-expect-error
62 expectType<Error>(result)
63 })
64 .catch((error) => {
65 // catch is always any-typed, nothing we can do here
66 })
67})()
68
69// More complex usage of thunk args
70;(async function () {
71 interface BookModel {
72 id: string
73 title: string
74 }
75
76 type BooksState = BookModel[]
77
78 const fakeBooks: BookModel[] = [
79 { id: 'b', title: 'Second' },
80 { id: 'a', title: 'First' },
81 ]
82
83 const correctDispatch = (() => {}) as ThunkDispatch<
84 BookModel[],
85 { userAPI: Function },
86 AnyAction
87 >
88
89 // Verify that the the first type args to createAsyncThunk line up right
90 const fetchBooksTAC = createAsyncThunk<
91 BookModel[],
92 number,
93 {
94 state: BooksState
95 extra: { userAPI: Function }
96 }
97 >(
98 'books/fetch',
99 async (arg, { getState, dispatch, extra, requestId, signal }) => {
100 const state = getState()
101
102 expectType<number>(arg)
103 expectType<BookModel[]>(state)
104 expectType<{ userAPI: Function }>(extra)
105 return fakeBooks
106 }
107 )
108
109 correctDispatch(fetchBooksTAC(1))
110 // @ts-expect-error
111 defaultDispatch(fetchBooksTAC(1))
112})()
113/**
114 * returning a rejected action from the promise creator is possible
115 */
116;(async () => {
117 type ReturnValue = { data: 'success' }
118 type RejectValue = { data: 'error' }
119
120 const fetchBooksTAC = createAsyncThunk<
121 ReturnValue,
122 number,
123 {
124 rejectValue: RejectValue
125 }
126 >('books/fetch', async (arg, { rejectWithValue }) => {
127 return rejectWithValue({ data: 'error' })
128 })
129
130 const returned = await defaultDispatch(fetchBooksTAC(1))
131 if (fetchBooksTAC.rejected.match(returned)) {
132 expectType<undefined | RejectValue>(returned.payload)
133 expectType<RejectValue>(returned.payload!)
134 } else {
135 expectType<ReturnValue>(returned.payload)
136 }
137
138 expectType<ReturnValue>(unwrapResult(returned))
139 // @ts-expect-error
140 expectType<RejectValue>(unwrapResult(returned))
141})()
142
143/**
144 * regression #1156: union return values fall back to allowing only single member
145 */
146;(async () => {
147 const fn = createAsyncThunk('session/isAdmin', async () => {
148 const response: boolean = false
149 return response
150 })
151})()
152
153/**
154 * Should handle reject withvalue within a try catch block
155 *
156 * Note:
157 * this is a sample code taken from #1605
158 *
159 */
160;(async () => {
161 type ResultType = {
162 text: string
163 }
164 const demoPromise = async (): Promise<ResultType> =>
165 new Promise((resolve, _) => resolve({ text: '' }))
166 const thunk = createAsyncThunk('thunk', async (args, thunkAPI) => {
167 try {
168 const result = await demoPromise()
169 return result
170 } catch (error) {
171 return thunkAPI.rejectWithValue(error)
172 }
173 })
174 createReducer({}, (builder) =>
175 builder.addCase(thunk.fulfilled, (s, action) => {
176 expectType<ResultType>(action.payload)
177 })
178 )
179})()
180
181{
182 interface Item {
183 name: string
184 }
185
186 interface ErrorFromServer {
187 error: string
188 }
189
190 interface CallsResponse {
191 data: Item[]
192 }
193
194 const fetchLiveCallsError = createAsyncThunk<
195 Item[],
196 string,
197 {
198 rejectValue: ErrorFromServer
199 }
200 >('calls/fetchLiveCalls', async (organizationId, { rejectWithValue }) => {
201 try {
202 const result = await apiRequest.get<CallsResponse>(
203 `organizations/${organizationId}/calls/live/iwill404`
204 )
205 return result.data.data
206 } catch (err) {
207 let error: AxiosError<ErrorFromServer> = err as any // cast for access to AxiosError properties
208 if (!error.response) {
209 // let it be handled as any other unknown error
210 throw err
211 }
212 return rejectWithValue(error.response && error.response.data)
213 }
214 })
215
216 defaultDispatch(fetchLiveCallsError('asd')).then((result) => {
217 if (fetchLiveCallsError.fulfilled.match(result)) {
218 //success
219 expectType<ReturnType<typeof fetchLiveCallsError['fulfilled']>>(result)
220 expectType<Item[]>(result.payload)
221 } else {
222 expectType<ReturnType<typeof fetchLiveCallsError['rejected']>>(result)
223 if (result.payload) {
224 // rejected with value
225 expectType<ErrorFromServer>(result.payload)
226 } else {
227 // rejected by throw
228 expectType<undefined>(result.payload)
229 expectType<SerializedError>(result.error)
230 // @ts-expect-error
231 expectType<IsAny<typeof result['error'], true, false>>(true)
232 }
233 }
234 defaultDispatch(fetchLiveCallsError('asd'))
235 .then((result) => {
236 expectType<Item[] | ErrorFromServer | undefined>(result.payload)
237 // @ts-expect-error
238 expectType<Item[]>(unwrapped)
239 return result
240 })
241 .then(unwrapResult)
242 .then((unwrapped) => {
243 expectType<Item[]>(unwrapped)
244 // @ts-expect-error
245 expectType<ErrorFromServer>(unwrapResult(unwrapped))
246 })
247 })
248}
249
250/**
251 * payloadCreator first argument type has impact on asyncThunk argument
252 */
253{
254 // no argument: asyncThunk has no argument
255 {
256 const asyncThunk = createAsyncThunk('test', () => 0)
257 expectType<() => any>(asyncThunk)
258 // @ts-expect-error cannot be called with an argument
259 asyncThunk(0 as any)
260 }
261
262 // one argument, specified as undefined: asyncThunk has no argument
263 {
264 const asyncThunk = createAsyncThunk('test', (arg: undefined) => 0)
265 expectType<() => any>(asyncThunk)
266 // @ts-expect-error cannot be called with an argument
267 asyncThunk(0 as any)
268 }
269
270 // one argument, specified as void: asyncThunk has no argument
271 {
272 const asyncThunk = createAsyncThunk('test', (arg: void) => 0)
273 expectType<() => any>(asyncThunk)
274 // @ts-expect-error cannot be called with an argument
275 asyncThunk(0 as any)
276 }
277
278 // one argument, specified as optional number: asyncThunk has optional number argument
279 // this test will fail with strictNullChecks: false, that is to be expected
280 // in that case, we have to forbid this behaviour or it will make arguments optional everywhere
281 {
282 const asyncThunk = createAsyncThunk('test', (arg?: number) => 0)
283 expectType<(arg?: number) => any>(asyncThunk)
284 asyncThunk()
285 asyncThunk(5)
286 // @ts-expect-error
287 asyncThunk('string')
288 }
289
290 // one argument, 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('test', (arg: number | undefined) => 0)
295 expectType<(arg?: number) => any>(asyncThunk)
296 asyncThunk()
297 asyncThunk(5)
298 // @ts-expect-error
299 asyncThunk('string')
300 }
301
302 // one argument, specified as number|void: asyncThunk has optional number argument
303 {
304 const asyncThunk = createAsyncThunk('test', (arg: number | void) => 0)
305 expectType<(arg?: number) => any>(asyncThunk)
306 asyncThunk()
307 asyncThunk(5)
308 // @ts-expect-error
309 asyncThunk('string')
310 }
311
312 // one argument, specified as any: asyncThunk has required any argument
313 {
314 const asyncThunk = createAsyncThunk('test', (arg: any) => 0)
315 expectType<IsAny<Parameters<typeof asyncThunk>[0], true, false>>(true)
316 asyncThunk(5)
317 // @ts-expect-error
318 asyncThunk()
319 }
320
321 // one argument, specified as unknown: asyncThunk has required unknown argument
322 {
323 const asyncThunk = createAsyncThunk('test', (arg: unknown) => 0)
324 expectType<IsUnknown<Parameters<typeof asyncThunk>[0], true, false>>(true)
325 asyncThunk(5)
326 // @ts-expect-error
327 asyncThunk()
328 }
329
330 // one argument, specified as number: asyncThunk has required number argument
331 {
332 const asyncThunk = createAsyncThunk('test', (arg: number) => 0)
333 expectType<(arg: number) => any>(asyncThunk)
334 asyncThunk(5)
335 // @ts-expect-error
336 asyncThunk()
337 }
338
339 // two arguments, first specified as undefined: asyncThunk has no argument
340 {
341 const asyncThunk = createAsyncThunk('test', (arg: undefined, thunkApi) => 0)
342 expectType<() => any>(asyncThunk)
343 // @ts-expect-error cannot be called with an argument
344 asyncThunk(0 as any)
345 }
346
347 // two arguments, first specified as void: asyncThunk has no argument
348 {
349 const asyncThunk = createAsyncThunk('test', (arg: void, thunkApi) => 0)
350 expectType<() => any>(asyncThunk)
351 // @ts-expect-error cannot be called with an argument
352 asyncThunk(0 as any)
353 }
354
355 // two arguments, first specified as number|undefined: asyncThunk has optional number argument
356 // this test will fail with strictNullChecks: false, that is to be expected
357 // in that case, we have to forbid this behaviour or it will make arguments optional everywhere
358 {
359 const asyncThunk = createAsyncThunk(
360 'test',
361 (arg: number | undefined, thunkApi) => 0
362 )
363 expectType<(arg?: number) => any>(asyncThunk)
364 asyncThunk()
365 asyncThunk(5)
366 // @ts-expect-error
367 asyncThunk('string')
368 }
369
370 // two arguments, first specified as number|void: asyncThunk has optional number argument
371 {
372 const asyncThunk = createAsyncThunk(
373 'test',
374 (arg: number | void, thunkApi) => 0
375 )
376 expectType<(arg?: number) => any>(asyncThunk)
377 asyncThunk()
378 asyncThunk(5)
379 // @ts-expect-error
380 asyncThunk('string')
381 }
382
383 // two arguments, first specified as any: asyncThunk has required any argument
384 {
385 const asyncThunk = createAsyncThunk('test', (arg: any, thunkApi) => 0)
386 expectType<IsAny<Parameters<typeof asyncThunk>[0], true, false>>(true)
387 asyncThunk(5)
388 // @ts-expect-error
389 asyncThunk()
390 }
391
392 // two arguments, first specified as unknown: asyncThunk has required unknown argument
393 {
394 const asyncThunk = createAsyncThunk('test', (arg: unknown, thunkApi) => 0)
395 expectType<IsUnknown<Parameters<typeof asyncThunk>[0], true, false>>(true)
396 asyncThunk(5)
397 // @ts-expect-error
398 asyncThunk()
399 }
400
401 // two arguments, first specified as number: asyncThunk has required number argument
402 {
403 const asyncThunk = createAsyncThunk('test', (arg: number, thunkApi) => 0)
404 expectType<(arg: number) => any>(asyncThunk)
405 asyncThunk(5)
406 // @ts-expect-error
407 asyncThunk()
408 }
409}
410
411{
412 // createAsyncThunk without generics
413 const thunk = createAsyncThunk('test', () => {
414 return 'ret' as const
415 })
416 expectType<AsyncThunk<'ret', void, {}>>(thunk)
417}
418
419{
420 // createAsyncThunk without generics, accessing `api` does not break return type
421 const thunk = createAsyncThunk('test', (_: void, api) => {
422 console.log(api)
423 return 'ret' as const
424 })
425 expectType<AsyncThunk<'ret', void, {}>>(thunk)
426}
427
428// createAsyncThunk rejectWithValue without generics: Expect correct return type
429{
430 const asyncThunk = createAsyncThunk(
431 'test',
432 (_: void, { rejectWithValue }) => {
433 try {
434 return Promise.resolve(true)
435 } catch (e) {
436 return rejectWithValue(e)
437 }
438 }
439 )
440
441 defaultDispatch(asyncThunk())
442 .then((result) => {
443 if (asyncThunk.fulfilled.match(result)) {
444 expectType<ReturnType<AsyncThunkFulfilledActionCreator<boolean, void>>>(
445 result
446 )
447 expectType<boolean>(result.payload)
448 // @ts-expect-error
449 expectType<any>(result.error)
450 } else {
451 expectType<ReturnType<AsyncThunkRejectedActionCreator<unknown, void>>>(
452 result
453 )
454 expectType<SerializedError>(result.error)
455 expectType<unknown>(result.payload)
456 }
457
458 return result
459 })
460 .then(unwrapResult)
461 .then((unwrapped) => {
462 expectType<boolean>(unwrapped)
463 })
464}
465
466{
467 type Funky = { somethingElse: 'Funky!' }
468 function funkySerializeError(err: any): Funky {
469 return { somethingElse: 'Funky!' }
470 }
471
472 // has to stay on one line or type tests fail in older TS versions
473 // prettier-ignore
474 // @ts-expect-error
475 const shouldFail = createAsyncThunk('without generics', () => {}, { serializeError: funkySerializeError })
476
477 const shouldWork = createAsyncThunk<
478 any,
479 void,
480 { serializedErrorType: Funky }
481 >('with generics', () => {}, {
482 serializeError: funkySerializeError,
483 })
484
485 if (shouldWork.rejected.match(anyAction)) {
486 expectType<Funky>(anyAction.error)
487 }
488}
489
490/**
491 * `idGenerator` option takes no arguments, and returns a string
492 */
493{
494 const returnsNumWithArgs = (foo: any) => 100
495 // has to stay on one line or type tests fail in older TS versions
496 // prettier-ignore
497 // @ts-expect-error
498 const shouldFailNumWithArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithArgs })
499
500 const returnsNumWithoutArgs = () => 100
501 // prettier-ignore
502 // @ts-expect-error
503 const shouldFailNumWithoutArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithoutArgs })
504
505 const returnsStrWithNumberArg = (foo: number) => 'foo'
506 // prettier-ignore
507 // @ts-expect-error
508 const shouldFailWrongArgs = createAsyncThunk('foo', (arg: string) => {}, { idGenerator: returnsStrWithNumberArg })
509
510 const returnsStrWithStringArg = (foo: string) => 'foo'
511 const shoulducceedCorrectArgs = createAsyncThunk('foo', (arg: string) => {}, {
512 idGenerator: returnsStrWithStringArg,
513 })
514
515 const returnsStrWithoutArgs = () => 'foo'
516 const shouldSucceed = createAsyncThunk('foo', () => {}, {
517 idGenerator: returnsStrWithoutArgs,
518 })
519}
520
521// meta return values
522{
523 // return values
524 createAsyncThunk<'ret', void, {}>('test', (_, api) => 'ret' as const)
525 createAsyncThunk<'ret', void, {}>('test', async (_, api) => 'ret' as const)
526 createAsyncThunk<'ret', void, { fulfilledMeta: string }>('test', (_, api) =>
527 api.fulfillWithValue('ret' as const, '')
528 )
529 createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
530 'test',
531 async (_, api) => api.fulfillWithValue('ret' as const, '')
532 )
533 createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
534 'test',
535 // @ts-expect-error has to be a fulfilledWithValue call
536 (_, api) => 'ret' as const
537 )
538 createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
539 'test',
540 // @ts-expect-error has to be a fulfilledWithValue call
541 async (_, api) => 'ret' as const
542 )
543 createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
544 'test', // @ts-expect-error should only allow returning with 'test'
545 (_, api) => api.fulfillWithValue(5, '')
546 )
547 createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
548 'test', // @ts-expect-error should only allow returning with 'test'
549 async (_, api) => api.fulfillWithValue(5, '')
550 )
551
552 // reject values
553 createAsyncThunk<'ret', void, { rejectValue: string }>('test', (_, api) =>
554 api.rejectWithValue('ret')
555 )
556 createAsyncThunk<'ret', void, { rejectValue: string }>(
557 'test',
558 async (_, api) => api.rejectWithValue('ret')
559 )
560 createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
561 'test',
562 (_, api) => api.rejectWithValue('ret', 5)
563 )
564 createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
565 'test',
566 async (_, api) => api.rejectWithValue('ret', 5)
567 )
568 createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
569 'test',
570 (_, api) => api.rejectWithValue('ret', 5)
571 )
572 createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
573 'test',
574 // @ts-expect-error wrong rejectedMeta type
575 (_, api) => api.rejectWithValue('ret', '')
576 )
577 createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
578 'test',
579 // @ts-expect-error wrong rejectedMeta type
580 async (_, api) => api.rejectWithValue('ret', '')
581 )
582 createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
583 'test',
584 // @ts-expect-error wrong rejectValue type
585 (_, api) => api.rejectWithValue(5, '')
586 )
587 createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
588 'test',
589 // @ts-expect-error wrong rejectValue type
590 async (_, api) => api.rejectWithValue(5, '')
591 )
592}
593
\No newline at end of file