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