UNPKG

12.7 kBPlain TextView Raw
1import {
2 ActionFromMatcher,
3 hasMatchFunction,
4 Matcher,
5 UnionToIntersection
6} from './tsHelpers'
7import {
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(action: any, validStatus: string[]) {
73 if (!action || !action.meta) return false
74
75 const hasValidRequestId = typeof action.meta.requestId === 'string'
76 const hasValidRequestStatus =
77 validStatus.indexOf(action.meta.requestStatus) > -1
78
79 return hasValidRequestId && hasValidRequestStatus
80}
81
82function isAsyncThunkArray(a: [any] | AnyAsyncThunk[]): a is AnyAsyncThunk[] {
83 return (
84 typeof a[0] === 'function' &&
85 'pending' in a[0] &&
86 'fulfilled' in a[0] &&
87 'rejected' in a[0]
88 )
89}
90
91export type UnknownAsyncThunkPendingAction = ReturnType<
92 AsyncThunkPendingActionCreator<unknown>
93>
94
95export type PendingActionFromAsyncThunk<
96 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<
158 T extends AnyAsyncThunk
159> = ActionFromMatcher<T['rejected']>
160
161/**
162 * A higher-order function that returns a function that may be used to check
163 * whether an action was created by an async thunk action creator, and that
164 * the action is rejected.
165 *
166 * @public
167 */
168export function isRejected(): (
169 action: any
170) => action is UnknownAsyncThunkRejectedAction
171/**
172 * A higher-order function that returns a function that may be used to check
173 * whether an action belongs to one of the provided async thunk action creators,
174 * and that the action is rejected.
175 *
176 * @param asyncThunks (optional) The async thunk action creators to match against.
177 *
178 * @public
179 */
180export function isRejected<
181 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
182>(
183 ...asyncThunks: AsyncThunks
184): (action: any) => action is RejectedActionFromAsyncThunk<AsyncThunks[number]>
185/**
186 * Tests if `action` is a rejected thunk action
187 * @public
188 */
189export function isRejected(
190 action: any
191): action is UnknownAsyncThunkRejectedAction
192export function isRejected<
193 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
194>(...asyncThunks: AsyncThunks | [any]) {
195 if (asyncThunks.length === 0) {
196 return (action: any) => hasExpectedRequestMetadata(action, ['rejected'])
197 }
198
199 if (!isAsyncThunkArray(asyncThunks)) {
200 return isRejected()(asyncThunks[0])
201 }
202
203 return (
204 action: any
205 ): action is RejectedActionFromAsyncThunk<AsyncThunks[number]> => {
206 // note: this type will be correct because we have at least 1 asyncThunk
207 const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
208 asyncThunk => asyncThunk.rejected
209 ) as any
210
211 const combinedMatcher = isAnyOf(...matchers)
212
213 return combinedMatcher(action)
214 }
215}
216
217export type UnknownAsyncThunkRejectedWithValueAction = ReturnType<
218 AsyncThunkRejectedActionCreator<unknown, unknown>
219>
220
221export type RejectedWithValueActionFromAsyncThunk<
222 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<
294 T extends AnyAsyncThunk
295> = ActionFromMatcher<T['fulfilled']>
296
297/**
298 * A higher-order function that returns a function that may be used to check
299 * whether an action was created by an async thunk action creator, and that
300 * the action is fulfilled.
301 *
302 * @public
303 */
304export function isFulfilled(): (
305 action: any
306) => action is UnknownAsyncThunkFulfilledAction
307/**
308 * A higher-order function that returns a function that may be used to check
309 * whether an action belongs to one of the provided async thunk action creators,
310 * and that the action is fulfilled.
311 *
312 * @param asyncThunks (optional) The async thunk action creators to match against.
313 *
314 * @public
315 */
316export function isFulfilled<
317 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
318>(
319 ...asyncThunks: AsyncThunks
320): (action: any) => action is FulfilledActionFromAsyncThunk<AsyncThunks[number]>
321/**
322 * Tests if `action` is a fulfilled thunk action
323 * @public
324 */
325export function isFulfilled(
326 action: any
327): action is UnknownAsyncThunkFulfilledAction
328export function isFulfilled<
329 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
330>(...asyncThunks: AsyncThunks | [any]) {
331 if (asyncThunks.length === 0) {
332 return (action: any) => hasExpectedRequestMetadata(action, ['fulfilled'])
333 }
334
335 if (!isAsyncThunkArray(asyncThunks)) {
336 return isFulfilled()(asyncThunks[0])
337 }
338
339 return (
340 action: any
341 ): action is FulfilledActionFromAsyncThunk<AsyncThunks[number]> => {
342 // note: this type will be correct because we have at least 1 asyncThunk
343 const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
344 asyncThunk => asyncThunk.fulfilled
345 ) as any
346
347 const combinedMatcher = isAnyOf(...matchers)
348
349 return combinedMatcher(action)
350 }
351}
352
353export type UnknownAsyncThunkAction =
354 | UnknownAsyncThunkPendingAction
355 | UnknownAsyncThunkRejectedAction
356 | UnknownAsyncThunkFulfilledAction
357
358export type AnyAsyncThunk = AsyncThunk<any, any, any>
359
360export type ActionsFromAsyncThunk<T extends AnyAsyncThunk> =
361 | ActionFromMatcher<T['pending']>
362 | ActionFromMatcher<T['fulfilled']>
363 | ActionFromMatcher<T['rejected']>
364
365/**
366 * A higher-order function that returns a function that may be used to check
367 * whether an action was created by an async thunk action creator.
368 *
369 * @public
370 */
371export function isAsyncThunkAction(): (
372 action: any
373) => action is UnknownAsyncThunkAction
374/**
375 * A higher-order function that returns a function that may be used to check
376 * whether an action belongs to one of the provided async thunk action creators.
377 *
378 * @param asyncThunks (optional) The async thunk action creators to match against.
379 *
380 * @public
381 */
382export function isAsyncThunkAction<
383 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
384>(
385 ...asyncThunks: AsyncThunks
386): (action: any) => action is ActionsFromAsyncThunk<AsyncThunks[number]>
387/**
388 * Tests if `action` is a thunk action
389 * @public
390 */
391export function isAsyncThunkAction(
392 action: any
393): action is UnknownAsyncThunkAction
394export function isAsyncThunkAction<
395 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
396>(...asyncThunks: AsyncThunks | [any]) {
397 if (asyncThunks.length === 0) {
398 return (action: any) =>
399 hasExpectedRequestMetadata(action, ['pending', 'fulfilled', 'rejected'])
400 }
401
402 if (!isAsyncThunkArray(asyncThunks)) {
403 return isAsyncThunkAction()(asyncThunks[0])
404 }
405
406 return (
407 action: any
408 ): action is ActionsFromAsyncThunk<AsyncThunks[number]> => {
409 // note: this type will be correct because we have at least 1 asyncThunk
410 const matchers: [Matcher<any>, ...Matcher<any>[]] = [] as any
411
412 for (const asyncThunk of asyncThunks) {
413 matchers.push(
414 asyncThunk.pending,
415 asyncThunk.rejected,
416 asyncThunk.fulfilled
417 )
418 }
419
420 const combinedMatcher = isAnyOf(...matchers)
421
422 return combinedMatcher(action)
423 }
424}
425
\No newline at end of file