UNPKG

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