UNPKG

12.9 kBPlain TextView Raw
1import type {
2 ActionFromMatcher,
3 Matcher,
4 UnionToIntersection,
5} from './tsHelpers'
6import { hasMatchFunction } from './tsHelpers'
7import type {
8 AsyncThunk,
9 AsyncThunkFulfilledActionCreator,
10 AsyncThunkPendingActionCreator,
11 AsyncThunkRejectedActionCreator,
12} from './createAsyncThunk'
13
14/** @public */
15export type ActionMatchingAnyOf<Matchers extends [...Matcher<any>[]]> =
16 ActionFromMatcher<Matchers[number]>
17
18/** @public */
19export type ActionMatchingAllOf<Matchers extends [...Matcher<any>[]]> =
20 UnionToIntersection<ActionMatchingAnyOf<Matchers>>
21
22const matches = (matcher: Matcher<any>, action: any) => {
23 if (hasMatchFunction(matcher)) {
24 return matcher.match(action)
25 } else {
26 return matcher(action)
27 }
28}
29
30/**
31 * A higher-order function that returns a function that may be used to check
32 * whether an action matches any one of the supplied type guards or action
33 * creators.
34 *
35 * @param matchers The type guards or action creators to match against.
36 *
37 * @public
38 */
39export function isAnyOf<Matchers extends [...Matcher<any>[]]>(
40 ...matchers: Matchers
41) {
42 return (action: any): action is ActionMatchingAnyOf<Matchers> => {
43 return matchers.some((matcher) => matches(matcher, action))
44 }
45}
46
47/**
48 * A higher-order function that returns a function that may be used to check
49 * whether an action matches all of the supplied type guards or action
50 * creators.
51 *
52 * @param matchers The type guards or action creators to match against.
53 *
54 * @public
55 */
56export function isAllOf<Matchers extends [...Matcher<any>[]]>(
57 ...matchers: Matchers
58) {
59 return (action: any): action is ActionMatchingAllOf<Matchers> => {
60 return matchers.every((matcher) => matches(matcher, action))
61 }
62}
63
64/**
65 * @param action A redux action
66 * @param validStatus An array of valid meta.requestStatus values
67 *
68 * @internal
69 */
70export function hasExpectedRequestMetadata(
71 action: any,
72 validStatus: readonly string[]
73) {
74 if (!action || !action.meta) return false
75
76 const hasValidRequestId = typeof action.meta.requestId === 'string'
77 const hasValidRequestStatus =
78 validStatus.indexOf(action.meta.requestStatus) > -1
79
80 return hasValidRequestId && hasValidRequestStatus
81}
82
83function isAsyncThunkArray(a: [any] | AnyAsyncThunk[]): a is AnyAsyncThunk[] {
84 return (
85 typeof a[0] === 'function' &&
86 'pending' in a[0] &&
87 'fulfilled' in a[0] &&
88 'rejected' in a[0]
89 )
90}
91
92export type UnknownAsyncThunkPendingAction = ReturnType<
93 AsyncThunkPendingActionCreator<unknown>
94>
95
96export type PendingActionFromAsyncThunk<T extends AnyAsyncThunk> =
97 ActionFromMatcher<T['pending']>
98
99/**
100 * A higher-order function that returns a function that may be used to check
101 * whether an action was created by an async thunk action creator, and that
102 * the action is pending.
103 *
104 * @public
105 */
106export function isPending(): (
107 action: any
108) => action is UnknownAsyncThunkPendingAction
109/**
110 * A higher-order function that returns a function that may be used to check
111 * whether an action belongs to one of the provided async thunk action creators,
112 * and that the action is pending.
113 *
114 * @param asyncThunks (optional) The async thunk action creators to match against.
115 *
116 * @public
117 */
118export function isPending<
119 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
120>(
121 ...asyncThunks: AsyncThunks
122): (action: any) => action is PendingActionFromAsyncThunk<AsyncThunks[number]>
123/**
124 * Tests if `action` is a pending thunk action
125 * @public
126 */
127export function isPending(action: any): action is UnknownAsyncThunkPendingAction
128export function isPending<
129 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
130>(...asyncThunks: AsyncThunks | [any]) {
131 if (asyncThunks.length === 0) {
132 return (action: any) => hasExpectedRequestMetadata(action, ['pending'])
133 }
134
135 if (!isAsyncThunkArray(asyncThunks)) {
136 return isPending()(asyncThunks[0])
137 }
138
139 return (
140 action: any
141 ): action is PendingActionFromAsyncThunk<AsyncThunks[number]> => {
142 // note: this type will be correct because we have at least 1 asyncThunk
143 const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
144 (asyncThunk) => asyncThunk.pending
145 ) as any
146
147 const combinedMatcher = isAnyOf(...matchers)
148
149 return combinedMatcher(action)
150 }
151}
152
153export type UnknownAsyncThunkRejectedAction = ReturnType<
154 AsyncThunkRejectedActionCreator<unknown, unknown>
155>
156
157export type RejectedActionFromAsyncThunk<T extends AnyAsyncThunk> =
158 ActionFromMatcher<T['rejected']>
159
160/**
161 * A higher-order function that returns a function that may be used to check
162 * whether an action was created by an async thunk action creator, and that
163 * the action is rejected.
164 *
165 * @public
166 */
167export function isRejected(): (
168 action: any
169) => action is UnknownAsyncThunkRejectedAction
170/**
171 * A higher-order function that returns a function that may be used to check
172 * whether an action belongs to one of the provided async thunk action creators,
173 * and that the action is rejected.
174 *
175 * @param asyncThunks (optional) The async thunk action creators to match against.
176 *
177 * @public
178 */
179export function isRejected<
180 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
181>(
182 ...asyncThunks: AsyncThunks
183): (action: any) => action is RejectedActionFromAsyncThunk<AsyncThunks[number]>
184/**
185 * Tests if `action` is a rejected thunk action
186 * @public
187 */
188export function isRejected(
189 action: any
190): action is UnknownAsyncThunkRejectedAction
191export function isRejected<
192 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
193>(...asyncThunks: AsyncThunks | [any]) {
194 if (asyncThunks.length === 0) {
195 return (action: any) => hasExpectedRequestMetadata(action, ['rejected'])
196 }
197
198 if (!isAsyncThunkArray(asyncThunks)) {
199 return isRejected()(asyncThunks[0])
200 }
201
202 return (
203 action: any
204 ): action is RejectedActionFromAsyncThunk<AsyncThunks[number]> => {
205 // note: this type will be correct because we have at least 1 asyncThunk
206 const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
207 (asyncThunk) => asyncThunk.rejected
208 ) as any
209
210 const combinedMatcher = isAnyOf(...matchers)
211
212 return combinedMatcher(action)
213 }
214}
215
216export type UnknownAsyncThunkRejectedWithValueAction = ReturnType<
217 AsyncThunkRejectedActionCreator<unknown, unknown>
218>
219
220export type RejectedWithValueActionFromAsyncThunk<T extends AnyAsyncThunk> =
221 ActionFromMatcher<T['rejected']> &
222 (T extends AsyncThunk<any, any, { rejectValue: infer RejectedValue }>
223 ? { payload: RejectedValue }
224 : unknown)
225
226/**
227 * A higher-order function that returns a function that may be used to check
228 * whether an action was created by an async thunk action creator, and that
229 * the action is rejected with value.
230 *
231 * @public
232 */
233export function isRejectedWithValue(): (
234 action: any
235) => action is UnknownAsyncThunkRejectedAction
236/**
237 * A higher-order function that returns a function that may be used to check
238 * whether an action belongs to one of the provided async thunk action creators,
239 * and that the action is rejected with value.
240 *
241 * @param asyncThunks (optional) The async thunk action creators to match against.
242 *
243 * @public
244 */
245export function isRejectedWithValue<
246 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
247>(
248 ...asyncThunks: AsyncThunks
249): (
250 action: any
251) => action is RejectedWithValueActionFromAsyncThunk<AsyncThunks[number]>
252/**
253 * Tests if `action` is a rejected thunk action with value
254 * @public
255 */
256export function isRejectedWithValue(
257 action: any
258): action is UnknownAsyncThunkRejectedAction
259export function isRejectedWithValue<
260 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
261>(...asyncThunks: AsyncThunks | [any]) {
262 const hasFlag = (action: any): action is any => {
263 return action && action.meta && action.meta.rejectedWithValue
264 }
265
266 if (asyncThunks.length === 0) {
267 return (action: any) => {
268 const combinedMatcher = isAllOf(isRejected(...asyncThunks), hasFlag)
269
270 return combinedMatcher(action)
271 }
272 }
273
274 if (!isAsyncThunkArray(asyncThunks)) {
275 return isRejectedWithValue()(asyncThunks[0])
276 }
277
278 return (
279 action: any
280 ): action is RejectedActionFromAsyncThunk<AsyncThunks[number]> => {
281 const combinedMatcher = isAllOf(isRejected(...asyncThunks), hasFlag)
282
283 return combinedMatcher(action)
284 }
285}
286
287export type UnknownAsyncThunkFulfilledAction = ReturnType<
288 AsyncThunkFulfilledActionCreator<unknown, unknown>
289>
290
291export type FulfilledActionFromAsyncThunk<T extends AnyAsyncThunk> =
292 ActionFromMatcher<T['fulfilled']>
293
294/**
295 * A higher-order function that returns a function that may be used to check
296 * whether an action was created by an async thunk action creator, and that
297 * the action is fulfilled.
298 *
299 * @public
300 */
301export function isFulfilled(): (
302 action: any
303) => action is UnknownAsyncThunkFulfilledAction
304/**
305 * A higher-order function that returns a function that may be used to check
306 * whether an action belongs to one of the provided async thunk action creators,
307 * and that the action is fulfilled.
308 *
309 * @param asyncThunks (optional) The async thunk action creators to match against.
310 *
311 * @public
312 */
313export function isFulfilled<
314 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
315>(
316 ...asyncThunks: AsyncThunks
317): (action: any) => action is FulfilledActionFromAsyncThunk<AsyncThunks[number]>
318/**
319 * Tests if `action` is a fulfilled thunk action
320 * @public
321 */
322export function isFulfilled(
323 action: any
324): action is UnknownAsyncThunkFulfilledAction
325export function isFulfilled<
326 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
327>(...asyncThunks: AsyncThunks | [any]) {
328 if (asyncThunks.length === 0) {
329 return (action: any) => hasExpectedRequestMetadata(action, ['fulfilled'])
330 }
331
332 if (!isAsyncThunkArray(asyncThunks)) {
333 return isFulfilled()(asyncThunks[0])
334 }
335
336 return (
337 action: any
338 ): action is FulfilledActionFromAsyncThunk<AsyncThunks[number]> => {
339 // note: this type will be correct because we have at least 1 asyncThunk
340 const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
341 (asyncThunk) => asyncThunk.fulfilled
342 ) as any
343
344 const combinedMatcher = isAnyOf(...matchers)
345
346 return combinedMatcher(action)
347 }
348}
349
350export type UnknownAsyncThunkAction =
351 | UnknownAsyncThunkPendingAction
352 | UnknownAsyncThunkRejectedAction
353 | UnknownAsyncThunkFulfilledAction
354
355export type AnyAsyncThunk = {
356 pending: { match: (action: any) => action is any }
357 fulfilled: { match: (action: any) => action is any }
358 rejected: { match: (action: any) => action is any }
359}
360
361export type ActionsFromAsyncThunk<T extends AnyAsyncThunk> =
362 | ActionFromMatcher<T['pending']>
363 | ActionFromMatcher<T['fulfilled']>
364 | ActionFromMatcher<T['rejected']>
365
366/**
367 * A higher-order function that returns a function that may be used to check
368 * whether an action was created by an async thunk action creator.
369 *
370 * @public
371 */
372export function isAsyncThunkAction(): (
373 action: any
374) => action is UnknownAsyncThunkAction
375/**
376 * A higher-order function that returns a function that may be used to check
377 * whether an action belongs to one of the provided async thunk action creators.
378 *
379 * @param asyncThunks (optional) The async thunk action creators to match against.
380 *
381 * @public
382 */
383export function isAsyncThunkAction<
384 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
385>(
386 ...asyncThunks: AsyncThunks
387): (action: any) => action is ActionsFromAsyncThunk<AsyncThunks[number]>
388/**
389 * Tests if `action` is a thunk action
390 * @public
391 */
392export function isAsyncThunkAction(
393 action: any
394): action is UnknownAsyncThunkAction
395export function isAsyncThunkAction<
396 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
397>(...asyncThunks: AsyncThunks | [any]) {
398 if (asyncThunks.length === 0) {
399 return (action: any) =>
400 hasExpectedRequestMetadata(action, ['pending', 'fulfilled', 'rejected'])
401 }
402
403 if (!isAsyncThunkArray(asyncThunks)) {
404 return isAsyncThunkAction()(asyncThunks[0])
405 }
406
407 return (
408 action: any
409 ): action is ActionsFromAsyncThunk<AsyncThunks[number]> => {
410 // note: this type will be correct because we have at least 1 asyncThunk
411 const matchers: [Matcher<any>, ...Matcher<any>[]] = [] as any
412
413 for (const asyncThunk of asyncThunks) {
414 matchers.push(
415 asyncThunk.pending,
416 asyncThunk.rejected,
417 asyncThunk.fulfilled
418 )
419 }
420
421 const combinedMatcher = isAnyOf(...matchers)
422
423 return combinedMatcher(action)
424 }
425}
426
\No newline at end of file