/**
* Redux action tools.
*
* @module redux
* @license Apache-2.0
* @copyright Mat. 2020-present
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import {
isString,
toBool,
type AllowSubset,
type Arr,
type Fun,
type NonConstEnum,
type Override,
} from "@xcmats/js-toolbox/type";
import { objectMap } from "@xcmats/js-toolbox/struct";
/**
* redux-compatible Action interface.
*/
export interface ReduxCompatAction<ActionType extends string = string> {
type: ActionType;
}
/**
* redux-compatible AnyAction interface.
*/
export interface ReduxCompatUnknownAction extends ReduxCompatAction {
[extraProps: string]: unknown;
}
/**
* Unique, private identifier distinguishing
* between EmptyAction and PayloadAction (statically and in runtime).
*/
const payload = Symbol("payload");
/**
* Empty action consists just of { type: ActionType } field.
*/
export interface EmptyAction<
ActionType extends string = string,
> extends ReduxCompatAction<ActionType> {
[payload]: false;
}
/**
* Action with payload: { type: ActionType, payload: PayloadType }
*/
export interface PayloadAction<
PayloadType = any,
ActionType extends string = string,
> extends ReduxCompatAction<ActionType> {
[payload]: true;
payload: PayloadType;
}
/**
* Empty action or action carrying payload.
*/
export type Action<
PayloadType = any,
ActionType extends string = string,
> =
| EmptyAction<ActionType>
| PayloadAction<PayloadType, ActionType>;
/**
* Type predicate - does a given action carry payload?
*/
export function isWithPayload<PayloadType, ActionType extends string> (
a: Action<PayloadType, ActionType>,
): a is PayloadAction<PayloadType, ActionType> {
return a[payload];
}
/**
* Action creator not carrying anything else than just `type` field.
*/
export interface EmptyActionCreator<
ActionType extends string,
> extends EmptyAction<ActionType> {
(): EmptyAction<ActionType>;
}
/**
* Action creator carrying payload (more than just `type`).
*/
export interface PayloadActionCreator<
PayloadType = any,
ActionType extends string = string,
Args extends Arr = Arr,
> extends EmptyAction<ActionType> {
(...args: Args): PayloadAction<PayloadType, ActionType>;
}
/**
* Any action creator (carrying just `type` or `type` and `payload`).
*/
export interface ActionCreator<
PayloadType = any,
ActionType extends string = string,
Args extends Arr = Arr,
> extends EmptyAction<ActionType> {
(...args: Args): Action<PayloadType, ActionType>;
}
/**
* Type predicate - not exposed red-g's internal.
*/
export function isACWithPayload<
PayloadType,
ActionType extends string,
Args extends Arr = Arr,
> (
a: ActionCreator<PayloadType, ActionType, Args>,
): a is PayloadActionCreator<PayloadType, ActionType, Args> {
return a[payload];
}
/**
* Redux action creator definer.
*
* @function defineActionCreator
* @param actionType Action type
* @param creator Optional custom function returning payload.
* @returns Action creator function.
*/
export function defineActionCreator<
ActionType extends string,
> (actionType: ActionType):
EmptyActionCreator<ActionType>;
export function defineActionCreator<
ActionType extends string,
PayloadType,
Args extends Arr,
> (actionType: ActionType, creator?: Fun<Args, PayloadType>):
PayloadActionCreator<PayloadType, ActionType, Args>;
export function defineActionCreator<
ActionType extends string,
PayloadType,
Args extends Arr,
> (actionType: ActionType, creator?: Fun<Args, PayloadType>):
ActionCreator<PayloadType, ActionType, Args> {
const actionCreator: any = !creator ?
() => ({
type: actionType,
[payload]: false,
}) :
(...args: Args) => ({
type: actionType,
[payload]: true,
payload: creator(...args),
});
actionCreator.type = actionType;
actionCreator[payload] = toBool(creator);
return actionCreator;
}
/**
* Construct interface based on `ActionEnum`. Consist of empty action
* creators (action creators without payload - just `type` field).
*/
export type EmptyActionCreators<
ActionEnum extends NonConstEnum<string, string>,
> = {
[K in keyof ActionEnum]: EmptyActionCreator<ActionEnum[K]>;
};
/**
* Construct object whose keys correspond to `actionEnum` keys and
* values consists of empty action creators for each type. Conforms to
* `EmptyActionCreators<ActionEnum>` interface.
*
* @function emptyActionCreators
* @param actionEnum Enum upon which an EmptyActionCreators object is built.
* @returns EmptyActionCreators object.
*/
export function emptyActionCreators<
ActionEnum extends NonConstEnum<string, string>,
> (
actionEnum: ActionEnum,
): EmptyActionCreators<ActionEnum> {
const actions = {} as EmptyActionCreators<ActionEnum>;
for (const actionType in actionEnum) {
actions[actionType] = defineActionCreator(actionEnum[actionType]);
}
return actions;
}
/**
* Take `ActionEnum` type with `PayloadCreators` object type and construct
* `PayloadActionCreators` on its basis.
*
* Constructed `PayloadActionCreators` object type consists only of keys
* that are also present in `ActionEnum` type (all other keys are dropped).
*/
export type PayloadActionCreators<
ActionEnum extends NonConstEnum<string, string>,
PayloadCreators,
> = {
[K in Extract<keyof PayloadCreators, keyof ActionEnum>]:
PayloadCreators[K] extends Fun<infer Args, infer PayloadType> ?
PayloadActionCreator<PayloadType, ActionEnum[K], Args> : never;
};
/**
* Take empty action creators object based on `ActionEnum` type
* (an object with all action creators not carrying anything besides
* `type` property) and `PayloadCreators` object consisting of plain
* javascript functions taking arguments and returning values.
*
* `PayloadCreators` object type is constrained to be a subset of `ActionEnum`
* type (in the sense of `AllowSubset` type defined in `type/utils.ts`).
*
* Create fully typed action creators object with all action creators
* defined as `EmptyActionCreator` or `PayloadActionCreator`.
*
* @function payloadActionCreators
* @param emptyActionCreators EmptyActionCreators object
* @param payloadCreators Object with payload creators.
* @returns ActionCreators object.
*/
export function payloadActionCreators<
ActionEnum extends NonConstEnum<string, string>,
PayloadCreators extends
& AllowSubset<ActionEnum, PayloadCreators>
& Partial<Record<keyof ActionEnum, Fun>>,
> (
emptyActionCreators: EmptyActionCreators<ActionEnum>,
payloadCreators: PayloadCreators,
):
Override<
typeof emptyActionCreators,
PayloadActionCreators<ActionEnum, PayloadCreators>
>
{
return Object.assign(
emptyActionCreators,
objectMap(payloadCreators) <keyof ActionEnum>(([key, creator]) => [
key, defineActionCreator(emptyActionCreators[key].type, creator),
]),
);
}
/**
* Construct action slice for provided action enum. Optionally
* define action creators with payload. Statically typed.
*
* @function actionCreators
* @param actionEnum Enum upon which an ActionCreators object is built.
* @param payloadCreators Optional object with payload creators.
* @returns ActionCreators object.
*/
export function actionCreators<
ActionEnum extends NonConstEnum<string, string>,
> (actionEnum: ActionEnum): EmptyActionCreators<ActionEnum>;
export function actionCreators<
ActionEnum extends NonConstEnum<string, string>,
PayloadCreators extends
& AllowSubset<ActionEnum, PayloadCreators>
& Partial<Record<keyof ActionEnum, Fun>>,
> (
actionEnum: ActionEnum,
payloadCreators?: PayloadCreators,
):
Override<
EmptyActionCreators<ActionEnum>,
PayloadActionCreators<ActionEnum, PayloadCreators>
>;
export function actionCreators<
ActionEnum extends NonConstEnum<string, string>,
PayloadCreators extends
& AllowSubset<ActionEnum, PayloadCreators>
& Partial<Record<keyof ActionEnum, Fun>>,
> (
actionEnum: ActionEnum,
payloadCreators?: PayloadCreators,
):
| EmptyActionCreators<ActionEnum>
| Override<
EmptyActionCreators<ActionEnum>,
PayloadActionCreators<ActionEnum, PayloadCreators>
>
{
const eac = emptyActionCreators(actionEnum);
if (payloadCreators) {
return payloadActionCreators(eac, payloadCreators);
} else {
return eac;
}
}
/**
* Dispatch-bound action creators and thunks contain type field.
*/
export type WithTypeField<T, ActionType extends string = string> =
& T
& { type: ActionType };
/**
* Checks for `type` field presence in a given candidate.
*/
export const isWithTypeField = <
T,
ActionType extends string = string,
>(c: T): c is WithTypeField<T, ActionType> => {
try {
return (
typeof c !== "undefined" &&
c != null &&
isString((c as { type?: ActionType }).type)
);
} catch {
return false;
}
};
/**
* Infer return type if `T` is function, return `T` itself otherwise.
*/
type ReturnTypeOrType<T> = T extends (...args: any) => infer R ? R : T;
/**
* Binds given action creator or thunk with chosen store's dispatch.
*
* @function bindActionCreator
* @param actionCreatorOrThunk any action creator or thunk
* @param dispatch redux store's `dispatch` function
* @returns bound action creator or thunk dispatch
*/
export function bindActionCreator<
AcIn extends any[], AcOut,
ActionType extends string = string,
> (
actionCreatorOrThunk: Fun<AcIn, AcOut>,
dispatch: Fun<[AcOut]>,
type?: ActionType,
): WithTypeField<Fun<AcIn, ReturnTypeOrType<AcOut>>, ActionType> {
const boundActionCreatorOrThunk =
(...args: AcIn) => dispatch(actionCreatorOrThunk(...args));
if (
isWithTypeField<typeof actionCreatorOrThunk, ActionType>(
actionCreatorOrThunk,
)
) {
boundActionCreatorOrThunk.type = actionCreatorOrThunk.type;
} else {
boundActionCreatorOrThunk.type = (
type ?? `${actionCreatorOrThunk.name}()`
) as ActionType;
}
return boundActionCreatorOrThunk;
}
/**
* Redux's original `bindActionCreators` clone with extended `Action`
* support (original function assumes dispatch parametrized with redux's
* `UnknownAction` which is not compatible with `Action`).
*
* Turns an object with action creators or thunks into an object with every
* action creator wrapped into a `dispatch` call.
*
* @function bindActionCreators
* @param actionCreators Object with action creator functions
* @param dispatch redux store's `dispatch` function
* @returns Object with wrapped action creators
*/
export function bindActionCreators<
ActionCreators extends { [P in keyof ActionCreators]: ActionCreators[P] },
AcOuts = ActionCreators[keyof ActionCreators],
BoundActionCreators = {
[P in keyof ActionCreators]:
WithTypeField<
Fun<
Parameters<ActionCreators[P]>,
ReturnTypeOrType<ReturnType<ActionCreators[P]>>
>
>
},
> (
actionCreators: ActionCreators,
dispatch: Fun<[AcOuts]>,
treeName?: string,
): BoundActionCreators {
return objectMap(actionCreators) (
([k, a]) => [
k,
bindActionCreator(
a, dispatch,
treeName ? `${treeName}.${String(k)}()` : `${String(k)}()`,
),
],
) as BoundActionCreators;
}
/**
* Bind whole tree of action creators to the redux's dispatch function.
*
* @function bindActionCreatorsTree
* @param acTree Object with `actionCreators` objects
* @param dispatch redux store's `dispatch` function
* @returns Object with wrapped action creators
*/
export function bindActionCreatorsTree<
ACTree extends { [P in keyof ACTree]: ACTree[P] },
ActionCreators = ACTree[keyof ACTree],
AcOuts = ActionCreators[keyof ActionCreators],
BoundACTree = {
[P in keyof ACTree]:
ReturnType<
typeof bindActionCreators<
ACTree[P], ACTree[P][keyof ACTree[P]]
>
>
},
> (
acTree: ACTree,
dispatch: Fun<[AcOuts]>,
): BoundACTree {
return objectMap(acTree) (
([k, a]) => [k, bindActionCreators(a, dispatch, String(k))],
) as BoundACTree;
}