import type { ActionFromMatcher, Matcher, UnionToIntersection, } from './tsHelpers' import { hasMatchFunction } from './tsHelpers' import type { AsyncThunk, AsyncThunkFulfilledActionCreator, AsyncThunkPendingActionCreator, AsyncThunkRejectedActionCreator, } from './createAsyncThunk' /** @public */ export type ActionMatchingAnyOf< Matchers extends [Matcher, ...Matcher[]] > = ActionFromMatcher /** @public */ export type ActionMatchingAllOf< Matchers extends [Matcher, ...Matcher[]] > = UnionToIntersection> const matches = (matcher: Matcher, action: any) => { if (hasMatchFunction(matcher)) { return matcher.match(action) } else { return matcher(action) } } /** * A higher-order function that returns a function that may be used to check * whether an action matches any one of the supplied type guards or action * creators. * * @param matchers The type guards or action creators to match against. * * @public */ export function isAnyOf, ...Matcher[]]>( ...matchers: Matchers ) { return (action: any): action is ActionMatchingAnyOf => { return matchers.some((matcher) => matches(matcher, action)) } } /** * A higher-order function that returns a function that may be used to check * whether an action matches all of the supplied type guards or action * creators. * * @param matchers The type guards or action creators to match against. * * @public */ export function isAllOf, ...Matcher[]]>( ...matchers: Matchers ) { return (action: any): action is ActionMatchingAllOf => { return matchers.every((matcher) => matches(matcher, action)) } } /** * @param action A redux action * @param validStatus An array of valid meta.requestStatus values * * @internal */ export function hasExpectedRequestMetadata( action: any, validStatus: readonly string[] ) { if (!action || !action.meta) return false const hasValidRequestId = typeof action.meta.requestId === 'string' const hasValidRequestStatus = validStatus.indexOf(action.meta.requestStatus) > -1 return hasValidRequestId && hasValidRequestStatus } function isAsyncThunkArray(a: [any] | AnyAsyncThunk[]): a is AnyAsyncThunk[] { return ( typeof a[0] === 'function' && 'pending' in a[0] && 'fulfilled' in a[0] && 'rejected' in a[0] ) } export type UnknownAsyncThunkPendingAction = ReturnType< AsyncThunkPendingActionCreator > export type PendingActionFromAsyncThunk = ActionFromMatcher /** * A higher-order function that returns a function that may be used to check * whether an action was created by an async thunk action creator, and that * the action is pending. * * @public */ export function isPending(): ( action: any ) => action is UnknownAsyncThunkPendingAction /** * A higher-order function that returns a function that may be used to check * whether an action belongs to one of the provided async thunk action creators, * and that the action is pending. * * @param asyncThunks (optional) The async thunk action creators to match against. * * @public */ export function isPending< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >( ...asyncThunks: AsyncThunks ): (action: any) => action is PendingActionFromAsyncThunk /** * Tests if `action` is a pending thunk action * @public */ export function isPending(action: any): action is UnknownAsyncThunkPendingAction export function isPending< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >(...asyncThunks: AsyncThunks | [any]) { if (asyncThunks.length === 0) { return (action: any) => hasExpectedRequestMetadata(action, ['pending']) } if (!isAsyncThunkArray(asyncThunks)) { return isPending()(asyncThunks[0]) } return ( action: any ): action is PendingActionFromAsyncThunk => { // note: this type will be correct because we have at least 1 asyncThunk const matchers: [Matcher, ...Matcher[]] = asyncThunks.map( (asyncThunk) => asyncThunk.pending ) as any const combinedMatcher = isAnyOf(...matchers) return combinedMatcher(action) } } export type UnknownAsyncThunkRejectedAction = ReturnType< AsyncThunkRejectedActionCreator > export type RejectedActionFromAsyncThunk = ActionFromMatcher /** * A higher-order function that returns a function that may be used to check * whether an action was created by an async thunk action creator, and that * the action is rejected. * * @public */ export function isRejected(): ( action: any ) => action is UnknownAsyncThunkRejectedAction /** * A higher-order function that returns a function that may be used to check * whether an action belongs to one of the provided async thunk action creators, * and that the action is rejected. * * @param asyncThunks (optional) The async thunk action creators to match against. * * @public */ export function isRejected< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >( ...asyncThunks: AsyncThunks ): (action: any) => action is RejectedActionFromAsyncThunk /** * Tests if `action` is a rejected thunk action * @public */ export function isRejected( action: any ): action is UnknownAsyncThunkRejectedAction export function isRejected< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >(...asyncThunks: AsyncThunks | [any]) { if (asyncThunks.length === 0) { return (action: any) => hasExpectedRequestMetadata(action, ['rejected']) } if (!isAsyncThunkArray(asyncThunks)) { return isRejected()(asyncThunks[0]) } return ( action: any ): action is RejectedActionFromAsyncThunk => { // note: this type will be correct because we have at least 1 asyncThunk const matchers: [Matcher, ...Matcher[]] = asyncThunks.map( (asyncThunk) => asyncThunk.rejected ) as any const combinedMatcher = isAnyOf(...matchers) return combinedMatcher(action) } } export type UnknownAsyncThunkRejectedWithValueAction = ReturnType< AsyncThunkRejectedActionCreator > export type RejectedWithValueActionFromAsyncThunk = ActionFromMatcher & (T extends AsyncThunk ? { payload: RejectedValue } : unknown) /** * A higher-order function that returns a function that may be used to check * whether an action was created by an async thunk action creator, and that * the action is rejected with value. * * @public */ export function isRejectedWithValue(): ( action: any ) => action is UnknownAsyncThunkRejectedAction /** * A higher-order function that returns a function that may be used to check * whether an action belongs to one of the provided async thunk action creators, * and that the action is rejected with value. * * @param asyncThunks (optional) The async thunk action creators to match against. * * @public */ export function isRejectedWithValue< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >( ...asyncThunks: AsyncThunks ): ( action: any ) => action is RejectedWithValueActionFromAsyncThunk /** * Tests if `action` is a rejected thunk action with value * @public */ export function isRejectedWithValue( action: any ): action is UnknownAsyncThunkRejectedAction export function isRejectedWithValue< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >(...asyncThunks: AsyncThunks | [any]) { const hasFlag = (action: any): action is any => { return action && action.meta && action.meta.rejectedWithValue } if (asyncThunks.length === 0) { return (action: any) => { const combinedMatcher = isAllOf(isRejected(...asyncThunks), hasFlag) return combinedMatcher(action) } } if (!isAsyncThunkArray(asyncThunks)) { return isRejectedWithValue()(asyncThunks[0]) } return ( action: any ): action is RejectedActionFromAsyncThunk => { const combinedMatcher = isAllOf(isRejected(...asyncThunks), hasFlag) return combinedMatcher(action) } } export type UnknownAsyncThunkFulfilledAction = ReturnType< AsyncThunkFulfilledActionCreator > export type FulfilledActionFromAsyncThunk = ActionFromMatcher /** * A higher-order function that returns a function that may be used to check * whether an action was created by an async thunk action creator, and that * the action is fulfilled. * * @public */ export function isFulfilled(): ( action: any ) => action is UnknownAsyncThunkFulfilledAction /** * A higher-order function that returns a function that may be used to check * whether an action belongs to one of the provided async thunk action creators, * and that the action is fulfilled. * * @param asyncThunks (optional) The async thunk action creators to match against. * * @public */ export function isFulfilled< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >( ...asyncThunks: AsyncThunks ): (action: any) => action is FulfilledActionFromAsyncThunk /** * Tests if `action` is a fulfilled thunk action * @public */ export function isFulfilled( action: any ): action is UnknownAsyncThunkFulfilledAction export function isFulfilled< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >(...asyncThunks: AsyncThunks | [any]) { if (asyncThunks.length === 0) { return (action: any) => hasExpectedRequestMetadata(action, ['fulfilled']) } if (!isAsyncThunkArray(asyncThunks)) { return isFulfilled()(asyncThunks[0]) } return ( action: any ): action is FulfilledActionFromAsyncThunk => { // note: this type will be correct because we have at least 1 asyncThunk const matchers: [Matcher, ...Matcher[]] = asyncThunks.map( (asyncThunk) => asyncThunk.fulfilled ) as any const combinedMatcher = isAnyOf(...matchers) return combinedMatcher(action) } } export type UnknownAsyncThunkAction = | UnknownAsyncThunkPendingAction | UnknownAsyncThunkRejectedAction | UnknownAsyncThunkFulfilledAction export type AnyAsyncThunk = { pending: { match: (action: any) => action is any } fulfilled: { match: (action: any) => action is any } rejected: { match: (action: any) => action is any } } export type ActionsFromAsyncThunk = | ActionFromMatcher | ActionFromMatcher | ActionFromMatcher /** * A higher-order function that returns a function that may be used to check * whether an action was created by an async thunk action creator. * * @public */ export function isAsyncThunkAction(): ( action: any ) => action is UnknownAsyncThunkAction /** * A higher-order function that returns a function that may be used to check * whether an action belongs to one of the provided async thunk action creators. * * @param asyncThunks (optional) The async thunk action creators to match against. * * @public */ export function isAsyncThunkAction< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >( ...asyncThunks: AsyncThunks ): (action: any) => action is ActionsFromAsyncThunk /** * Tests if `action` is a thunk action * @public */ export function isAsyncThunkAction( action: any ): action is UnknownAsyncThunkAction export function isAsyncThunkAction< AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]] >(...asyncThunks: AsyncThunks | [any]) { if (asyncThunks.length === 0) { return (action: any) => hasExpectedRequestMetadata(action, ['pending', 'fulfilled', 'rejected']) } if (!isAsyncThunkArray(asyncThunks)) { return isAsyncThunkAction()(asyncThunks[0]) } return ( action: any ): action is ActionsFromAsyncThunk => { // note: this type will be correct because we have at least 1 asyncThunk const matchers: [Matcher, ...Matcher[]] = [] as any for (const asyncThunk of asyncThunks) { matchers.push( asyncThunk.pending, asyncThunk.rejected, asyncThunk.fulfilled ) } const combinedMatcher = isAnyOf(...matchers) return combinedMatcher(action) } }