UNPKG

10.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 isAnyOf(...asyncThunks.map((asyncThunk) => asyncThunk.pending))
140}
141
142export type UnknownAsyncThunkRejectedAction = ReturnType<
143 AsyncThunkRejectedActionCreator<unknown, unknown>
144>
145
146export type RejectedActionFromAsyncThunk<T extends AnyAsyncThunk> =
147 ActionFromMatcher<T['rejected']>
148
149/**
150 * A higher-order function that returns a function that may be used to check
151 * whether an action was created by an async thunk action creator, and that
152 * the action is rejected.
153 *
154 * @public
155 */
156export function isRejected(): (
157 action: any,
158) => action is UnknownAsyncThunkRejectedAction
159/**
160 * A higher-order function that returns a function that may be used to check
161 * whether an action belongs to one of the provided async thunk action creators,
162 * and that the action is rejected.
163 *
164 * @param asyncThunks (optional) The async thunk action creators to match against.
165 *
166 * @public
167 */
168export function isRejected<
169 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]],
170>(
171 ...asyncThunks: AsyncThunks
172): (action: any) => action is RejectedActionFromAsyncThunk<AsyncThunks[number]>
173/**
174 * Tests if `action` is a rejected thunk action
175 * @public
176 */
177export function isRejected(
178 action: any,
179): action is UnknownAsyncThunkRejectedAction
180export function isRejected<
181 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]],
182>(...asyncThunks: AsyncThunks | [any]) {
183 if (asyncThunks.length === 0) {
184 return (action: any) => hasExpectedRequestMetadata(action, ['rejected'])
185 }
186
187 if (!isAsyncThunkArray(asyncThunks)) {
188 return isRejected()(asyncThunks[0])
189 }
190
191 return isAnyOf(...asyncThunks.map((asyncThunk) => asyncThunk.rejected))
192}
193
194export type UnknownAsyncThunkRejectedWithValueAction = ReturnType<
195 AsyncThunkRejectedActionCreator<unknown, unknown>
196>
197
198export type RejectedWithValueActionFromAsyncThunk<T extends AnyAsyncThunk> =
199 ActionFromMatcher<T['rejected']> &
200 (T extends AsyncThunk<any, any, { rejectValue: infer RejectedValue }>
201 ? { payload: RejectedValue }
202 : unknown)
203
204/**
205 * A higher-order function that returns a function that may be used to check
206 * whether an action was created by an async thunk action creator, and that
207 * the action is rejected with value.
208 *
209 * @public
210 */
211export function isRejectedWithValue(): (
212 action: any,
213) => action is UnknownAsyncThunkRejectedAction
214/**
215 * A higher-order function that returns a function that may be used to check
216 * whether an action belongs to one of the provided async thunk action creators,
217 * and that the action is rejected with value.
218 *
219 * @param asyncThunks (optional) The async thunk action creators to match against.
220 *
221 * @public
222 */
223export function isRejectedWithValue<
224 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]],
225>(
226 ...asyncThunks: AsyncThunks
227): (
228 action: any,
229) => action is RejectedWithValueActionFromAsyncThunk<AsyncThunks[number]>
230/**
231 * Tests if `action` is a rejected thunk action with value
232 * @public
233 */
234export function isRejectedWithValue(
235 action: any,
236): action is UnknownAsyncThunkRejectedAction
237export function isRejectedWithValue<
238 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]],
239>(...asyncThunks: AsyncThunks | [any]) {
240 const hasFlag = (action: any): action is any => {
241 return action && action.meta && action.meta.rejectedWithValue
242 }
243
244 if (asyncThunks.length === 0) {
245 return isAllOf(isRejected(...asyncThunks), hasFlag)
246 }
247
248 if (!isAsyncThunkArray(asyncThunks)) {
249 return isRejectedWithValue()(asyncThunks[0])
250 }
251
252 return isAllOf(isRejected(...asyncThunks), hasFlag)
253}
254
255export type UnknownAsyncThunkFulfilledAction = ReturnType<
256 AsyncThunkFulfilledActionCreator<unknown, unknown>
257>
258
259export type FulfilledActionFromAsyncThunk<T extends AnyAsyncThunk> =
260 ActionFromMatcher<T['fulfilled']>
261
262/**
263 * A higher-order function that returns a function that may be used to check
264 * whether an action was created by an async thunk action creator, and that
265 * the action is fulfilled.
266 *
267 * @public
268 */
269export function isFulfilled(): (
270 action: any,
271) => action is UnknownAsyncThunkFulfilledAction
272/**
273 * A higher-order function that returns a function that may be used to check
274 * whether an action belongs to one of the provided async thunk action creators,
275 * and that the action is fulfilled.
276 *
277 * @param asyncThunks (optional) The async thunk action creators to match against.
278 *
279 * @public
280 */
281export function isFulfilled<
282 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]],
283>(
284 ...asyncThunks: AsyncThunks
285): (action: any) => action is FulfilledActionFromAsyncThunk<AsyncThunks[number]>
286/**
287 * Tests if `action` is a fulfilled thunk action
288 * @public
289 */
290export function isFulfilled(
291 action: any,
292): action is UnknownAsyncThunkFulfilledAction
293export function isFulfilled<
294 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]],
295>(...asyncThunks: AsyncThunks | [any]) {
296 if (asyncThunks.length === 0) {
297 return (action: any) => hasExpectedRequestMetadata(action, ['fulfilled'])
298 }
299
300 if (!isAsyncThunkArray(asyncThunks)) {
301 return isFulfilled()(asyncThunks[0])
302 }
303
304 return isAnyOf(...asyncThunks.map((asyncThunk) => asyncThunk.fulfilled))
305}
306
307export type UnknownAsyncThunkAction =
308 | UnknownAsyncThunkPendingAction
309 | UnknownAsyncThunkRejectedAction
310 | UnknownAsyncThunkFulfilledAction
311
312export type AnyAsyncThunk = {
313 pending: { match: (action: any) => action is any }
314 fulfilled: { match: (action: any) => action is any }
315 rejected: { match: (action: any) => action is any }
316}
317
318export type ActionsFromAsyncThunk<T extends AnyAsyncThunk> =
319 | ActionFromMatcher<T['pending']>
320 | ActionFromMatcher<T['fulfilled']>
321 | ActionFromMatcher<T['rejected']>
322
323/**
324 * A higher-order function that returns a function that may be used to check
325 * whether an action was created by an async thunk action creator.
326 *
327 * @public
328 */
329export function isAsyncThunkAction(): (
330 action: any,
331) => action is UnknownAsyncThunkAction
332/**
333 * A higher-order function that returns a function that may be used to check
334 * whether an action belongs to one of the provided async thunk action creators.
335 *
336 * @param asyncThunks (optional) The async thunk action creators to match against.
337 *
338 * @public
339 */
340export function isAsyncThunkAction<
341 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]],
342>(
343 ...asyncThunks: AsyncThunks
344): (action: any) => action is ActionsFromAsyncThunk<AsyncThunks[number]>
345/**
346 * Tests if `action` is a thunk action
347 * @public
348 */
349export function isAsyncThunkAction(
350 action: any,
351): action is UnknownAsyncThunkAction
352export function isAsyncThunkAction<
353 AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]],
354>(...asyncThunks: AsyncThunks | [any]) {
355 if (asyncThunks.length === 0) {
356 return (action: any) =>
357 hasExpectedRequestMetadata(action, ['pending', 'fulfilled', 'rejected'])
358 }
359
360 if (!isAsyncThunkArray(asyncThunks)) {
361 return isAsyncThunkAction()(asyncThunks[0])
362 }
363
364 return isAnyOf(...asyncThunks.flatMap(asyncThunk => [asyncThunk.pending, asyncThunk.rejected, asyncThunk.fulfilled]))
365}
366
\No newline at end of file