///
import { Component } from 'react';
import {
compose,
AnyAction,
Dispatch as ReduxDispatch,
Reducer as ReduxReducer,
Store as ReduxStore,
StoreEnhancer,
Middleware,
Observable,
} from 'redux';
import { O } from 'ts-toolbelt';
export type ReduxAction = AnyAction;
/**
* Picks only the keys of a certain type
*/
type KeysOfType = {
[K in keyof A]-?: A[K] extends B ? K : never;
}[keyof A];
/**
* This allows you to narrow keys of an object type that are index signature
* based.
*
* Based on answer from here:
* https://stackoverflow.com/questions/56422807/narrowing-a-type-to-its-properties-that-are-index-signatures/56423972#56423972
*/
type IndexSignatureKeysOfType = {
[K in keyof A]: A[K] extends { [key: string]: any } | { [key: number]: any }
? string extends keyof A[K]
? K
: number extends keyof A[K]
? K
: never
: never;
}[keyof A];
type InvalidObjectTypes = string | Array | RegExp | Date | Function;
type IncludesDeep3 = O.Includes<
Obj,
M
> extends 1
? 1
: O.Includes<
{
[P in keyof Obj]: Obj[P] extends object ? O.Includes : 0;
},
1
>;
type IncludesDeep2 = O.Includes<
Obj,
M
> extends 1
? 1
: O.Includes<
{
[P in keyof Obj]: Obj[P] extends object ? IncludesDeep3 : 0;
},
1
>;
type IncludesDeep = O.Includes<
Obj,
M
> extends 1
? 1
: O.Includes<
{
[P in keyof Obj]: Obj[P] extends object ? IncludesDeep2 : 0;
},
1
>;
type StateResolver<
Model extends object,
StoreModel extends object,
Result = any
> = (state: State, storeState: State) => Result;
type StateResolvers =
| []
| [StateResolver]
| [
StateResolver,
StateResolver,
]
| [
StateResolver,
StateResolver,
StateResolver,
]
| [
StateResolver,
StateResolver,
StateResolver,
StateResolver,
]
| [
StateResolver,
StateResolver,
StateResolver,
StateResolver,
StateResolver,
]
| [
StateResolver,
StateResolver,
StateResolver,
StateResolver,
StateResolver,
StateResolver,
];
type AnyFunction = (...args: any[]) => any;
type ActionEmitterTypes = Action | Thunk;
type ActionListenerTypes = ActionOn | ThunkOn;
type ActionTypes =
| ActionEmitterTypes
| ActionListenerTypes
| Unstable_EffectOn;
interface ActionCreator {
(payload: Payload): void;
type: string;
z__creator: 'actionWithPayload';
}
interface ThunkCreator {
(payload: Payload): Result;
type: string;
startType: string;
successType: string;
failType: string;
z__creator: 'thunkWithPayload';
}
type ActionOrThunkCreator =
| ActionCreator
| ThunkCreator;
type Helpers = {
dispatch: Dispatch;
getState: () => State;
getStoreActions: () => Actions;
getStoreState: () => State;
injections: Injections;
meta: Meta;
};
// #region Helpers
export function debug(
state: StateDraft,
): StateDraft;
export function memo(fn: Fn, cacheSize: number): Fn;
// #endregion
// #region Listeners
type ValidListenerProperties = {
[P in keyof ActionsModel]: P extends IndexSignatureKeysOfType
? never
: ActionsModel[P] extends ActionListenerTypes
? P
: ActionsModel[P] extends object
? IncludesDeep extends 1
? P
: never
: never;
}[keyof ActionsModel];
type ListenerMapper<
ActionsModel extends object,
K extends keyof ActionsModel
> = {
[P in K]: ActionsModel[P] extends ActionOn
? ActionCreator>
: ActionsModel[P] extends ThunkOn
? ThunkCreator, any>
: ActionsModel[P] extends object
? RecursiveListeners
: ActionsModel[P];
};
type RecursiveListeners = ListenerMapper<
ActionsModel,
ValidListenerProperties
>;
/**
* Filters a model into a type that represents the listeners actions/thunks
*
* @example
*
* type OnlyListeners = Listeners;
*/
export type Listeners = RecursiveListeners;
// #endregion
// #region Actions
type ValidActionProperties = {
[P in keyof ActionsModel]: P extends IndexSignatureKeysOfType
? never
: ActionsModel[P] extends ActionEmitterTypes
? P
: ActionsModel[P] extends object
? IncludesDeep extends 1
? P
: never
: never;
}[keyof ActionsModel];
type ActionMapper = {
[P in K]: ActionsModel[P] extends Action
? ActionCreator
: ActionsModel[P] extends Thunk
? ActionsModel[P]['payload'] extends void
? ThunkCreator
: ThunkCreator
: ActionsModel[P] extends object
? RecursiveActions
: ActionsModel[P];
};
type RecursiveActions = ActionMapper<
ActionsModel,
ValidActionProperties
>;
/**
* Filters a model into a type that represents the action/thunk creators
*
* @example
*
* type OnlyActions = Actions;
*/
export type Actions = RecursiveActions;
// #endregion
// #region State
type StateTypes = Computed | Reducer | ActionTypes;
type StateMapper = {
[P in keyof StateModel]: StateModel[P] extends Generic
? T
: P extends IndexSignatureKeysOfType
? StateModel[P]
: StateModel[P] extends Computed
? StateModel[P]['result']
: StateModel[P] extends Reducer
? StateModel[P]['result']
: StateModel[P] extends object
? StateModel[P] extends InvalidObjectTypes
? StateModel[P]
: IncludesDeep extends 1
? RecursiveState
: StateModel[P]
: StateModel[P];
};
type RecursiveState = StateMapper<
O.Filter
>;
/**
* Filters a model into a type that represents the state only (i.e. no actions)
*
* @example
*
* type StateOnly = State;
*/
export type State = RecursiveState;
// #endregion
// #region Store + Config + Creation
/**
* Creates an easy-peasy powered Redux store.
*
* https://github.com/ctrlplusb/easy-peasy#createstoremodel-config
*
* @example
*
* import { createStore } from 'easy-peasy';
*
* interface StoreModel {
* todos: {
* items: Array;
* }
* }
*
* const store = createStore({
* todos: {
* items: [],
* }
* })
*/
export function createStore<
StoreModel extends object = {},
InitialState extends undefined | object = undefined,
Injections = any
>(
model: StoreModel,
config?: EasyPeasyConfig,
): Store>;
/**
* Configuration interface for the createStore
*/
export interface EasyPeasyConfig<
InitialState extends undefined | object = undefined,
Injections = any
> {
compose?: typeof compose;
devTools?: boolean;
disableImmer?: boolean;
enhancers?: StoreEnhancer[];
initialState?: InitialState;
injections?: Injections;
middleware?: Array>;
mockActions?: boolean;
name?: string;
reducerEnhancer?: (reducer: ReduxReducer) => ReduxReducer;
postActionReducer?: ReduxReducer;
}
export interface MockedAction {
type: string;
[key: string]: any;
}
export interface AddModelResult {
resolveRehydration: () => Promise;
}
/**
* Represents a Redux store, enhanced by easy peasy.
*
* @example
*
* import { Store } from 'easy-peasy';
* import { StoreModel } from './store';
*
* type EnhancedReduxStore = Store;
*/
export interface Store<
StoreModel extends object = {},
StoreConfig extends EasyPeasyConfig = EasyPeasyConfig<
undefined,
any
>
> extends ReduxStore> {
addModel: (
key: string,
modelSlice: ModelSlice,
) => AddModelResult;
clearMockedActions: () => void;
dispatch: Dispatch;
getActions: () => Actions;
getListeners: () => Listeners;
getMockedActions: () => MockedAction[];
persist: {
clear: () => Promise;
flush: () => Promise;
resolveRehydration: () => Promise;
};
reconfigure: (model: NewStoreModel) => void;
removeModel: (key: string) => void;
/**
* Interoperability point for observable/reactive libraries.
* @returns {observable} A minimal observable of state changes.
* For more information, see the observable proposal:
* https://github.com/tc39/proposal-observable
*/
[Symbol.observable](): Observable>;
}
// #endregion
// #region Dispatch
/**
* Enhanced version of the Redux Dispatch with action creators bound to it
*
* @example
*
* import { Dispatch } from 'easy-peasy';
* import { StoreModel } from './store';
*
* type DispatchWithActions = Dispatch;
*/
export type Dispatch<
StoreModel extends object = {},
Action extends ReduxAction = AnyAction
> = Actions & ReduxDispatch;
// #endregion
// #region Types shared by ActionOn and ThunkOn
type Target = ActionOrThunkCreator | string;
type TargetResolver = (
actions: Actions,
storeActions: Actions,
) => Target | Array;
interface TargetPayload {
type: string;
payload: Payload;
result?: any;
error?: Error;
resolvedTargets: Array;
}
type PayloadFromResolver<
Resolver extends TargetResolver,
Resolved = ReturnType
> = Resolved extends string
? any
: Resolved extends ActionOrThunkCreator
? Payload
: Resolved extends Array
? T extends string
? any
: T extends ActionOrThunkCreator
? Payload
: T
: unknown;
// #endregion
// #region Thunk
type Meta = {
path: string[];
parent: string[];
};
/**
* A thunk type.
*
* Useful when declaring your model.
*
* @example
*
* import { Thunk } from 'easy-peasy';
*
* interface TodosModel {
* todos: Array;
* addTodo: Thunk;
* }
*/
export type Thunk<
Model extends object,
Payload = void,
Injections = any,
StoreModel extends object = {},
Result = any
> = {
type: 'thunk';
payload: Payload;
result: Result;
};
/**
* Declares an thunk action type against your model.
*
* https://github.com/ctrlplusb/easy-peasy#thunkaction
*
* @example
*
* import { thunk } from 'easy-peasy';
*
* const store = createStore({
* login: thunk(async (actions, payload) => {
* const user = await loginService(payload);
* actions.loginSucceeded(user);
* })
* });
*/
export function thunk<
Model extends object = {},
Payload = void,
Injections = any,
StoreModel extends object = {},
Result = any
>(
thunk: (
actions: Actions,
payload: Payload,
helpers: {
dispatch: Dispatch;
getState: () => State;
getStoreActions: () => Actions;
getStoreState: () => State;
injections: Injections;
meta: Meta;
},
) => Result,
): Thunk;
// #endregion
// #region Listener Thunk
export interface ThunkOn<
Model extends object,
Injections = any,
StoreModel extends object = {}
> {
type: 'thunkOn';
}
export function thunkOn<
Model extends object = {},
Injections = any,
StoreModel extends object = {},
Resolver extends TargetResolver = TargetResolver<
Model,
StoreModel
>
>(
targetResolver: Resolver,
handler: (
actions: Actions,
target: TargetPayload>,
helpers: Helpers,
) => any,
): ThunkOn;
// #endregion
// #region Action
/**
* Represents an action.
*
* @example
*
* import { Action } from 'easy-peasy';
*
* interface Model {
* todos: Array;
* addTodo: Action;
* }
*/
export type Action = {
type: 'action';
payload: Payload;
result: void | State;
};
/**
* Declares an action.
*
* https://easy-peasy.now.sh/docs/api/action
*
* @example
*
* import { action } from 'easy-peasy';
*
* const store = createStore({
* count: 0,
* increment: action((state)) => {
* state.count += 1;
* })
* });
*/
export function action(
action: (state: State, payload: Payload) => void | State,
): Action;
// #endregion
// #region Listener Action
export interface ActionOn<
Model extends object = {},
StoreModel extends object = {}
> {
type: 'actionOn';
result: void | State;
}
export function actionOn<
Model extends object,
StoreModel extends object,
Resolver extends TargetResolver
>(
targetResolver: Resolver,
handler: (
state: State,
target: TargetPayload>,
) => void | State,
): ActionOn;
// #endregion
// #region Computed
/**
* Represents a computed property.
*
* @example
*
* import { Computed } from 'easy-peasy';
*
* interface Model {
* products: Array;
* totalPrice: Computed;
* }
*/
export type Computed<
Model extends object,
Result,
StoreModel extends object = {}
> = {
type: 'computed';
result: Result;
};
type DefaultComputationFunc = (
state: State,
) => Result;
export function computed<
Model extends object = {},
Result = void,
StoreModel extends object = {},
Resolvers extends StateResolvers = StateResolvers<
Model,
StoreModel
>
>(
resolversOrCompFunc: Resolvers | DefaultComputationFunc,
compFunc?: Resolvers extends [AnyFunction]
? (arg0: ReturnType) => Result
: Resolvers extends [AnyFunction, AnyFunction]
? (arg0: ReturnType, arg1: ReturnType) => Result
: Resolvers extends [AnyFunction, AnyFunction, AnyFunction]
? (
arg0: ReturnType,
arg1: ReturnType,
arg2: ReturnType,
) => Result
: Resolvers extends [AnyFunction, AnyFunction, AnyFunction, AnyFunction]
? (
arg0: ReturnType,
arg1: ReturnType,
arg2: ReturnType,
arg3: ReturnType,
) => Result
: Resolvers extends [
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
]
? (
arg0: ReturnType,
arg1: ReturnType,
arg2: ReturnType,
arg3: ReturnType,
arg4: ReturnType,
) => Result
: Resolvers extends [
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
]
? (
arg0: ReturnType,
arg1: ReturnType,
arg2: ReturnType,
arg3: ReturnType,
arg4: ReturnType,
arg5: ReturnType,
) => Result
: () => Result,
): Computed;
// #endregion
// #region EffectOn
export type Unstable_EffectOn<
Model extends object = {},
StoreModel extends object = {},
Injections = any
> = {
type: 'effectOn';
};
type DependencyResolver = (state: State) => any;
type Dependencies<
Resolvers extends StateResolvers
> = Resolvers extends [AnyFunction]
? [ReturnType]
: Resolvers extends [AnyFunction, AnyFunction]
? [ReturnType, ReturnType]
: Resolvers extends [AnyFunction, AnyFunction, AnyFunction]
? [
ReturnType,
ReturnType,
ReturnType,
]
: Resolvers extends [AnyFunction, AnyFunction, AnyFunction, AnyFunction]
? [
ReturnType,
ReturnType,
ReturnType,
ReturnType,
]
: Resolvers extends [
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
]
? [
ReturnType,
ReturnType,
ReturnType,
ReturnType,
ReturnType,
]
: Resolvers extends [
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
AnyFunction,
]
? [
ReturnType,
ReturnType,
ReturnType,
ReturnType,
ReturnType,
ReturnType,
]
: any[];
type Change> = {
prev: Dependencies;
current: Dependencies;
action: {
type: string;
payload?: any;
};
};
export type Dispose = () => any;
export function unstable_effectOn<
Model extends object = {},
StoreModel extends object = {},
Injections = any,
Resolvers extends StateResolvers = StateResolvers<
Model,
StoreModel
>
>(
dependencies: Resolvers,
effect: (
actions: Actions,
change: Change,
helpers: Helpers,
) => undefined | Dispose,
): Unstable_EffectOn;
// #endregion
// #region Reducer
/**
* A reducer type.
*
* Useful when declaring your model.
*
* @example
*
* import { Reducer } from 'easy-peasy';
*
* interface Model {
* router: Reducer;
* }
*/
export type Reducer = {
type: 'reducer';
result: State;
};
/**
* Allows you to declare a custom reducer to manage a bit of your state.
*
* https://github.com/ctrlplusb/easy-peasy#reducerfn
*
* @example
*
* import { reducer } from 'easy-peasy';
*
* const store = createStore({
* counter: reducer((state = 1, action) => {
* switch (action.type) {
* case 'INCREMENT': return state + 1;
* default: return state;
* }
* })
* });
*/
export function reducer(state: ReduxReducer): Reducer;
// #endregion
// #region Generics
/**
* Used to declare generic state on a model.
*
* @example
*
* interface MyGenericModel {
* value: Generic;
* setValue: Action, T>;
* }
*
* const numberModel: MyGenericModel = {
* value: generic(1337),
* setValue: action((state, value) => {
* state.value = value;
* })
* };
*/
export class Generic {
type: 'ezpz__generic';
}
/**
* Used to assign a generic state value against a model.
*
* @example
*
* interface MyGenericModel {
* value: Generic;
* setValue: Action, T>;
* }
*
* const numberModel: MyGenericModel = {
* value: generic(1337),
* setValue: action((state, value) => {
* state.value = value;
* })
* };
*/
export function generic(value: T): Generic;
// #endregion Generics
// #region Hooks
/**
* A React Hook allowing you to use state within your component.
*
* https://easy-peasy.now.sh/docs/api/use-store-state.html
*
* Note: you can create a pre-typed version of this hook via "createTypedHooks"
*
* @example
*
* import { useStoreState, State } from 'easy-peasy';
* import { StoreModel } from './store';
*
* function MyComponent() {
* const todos = useStoreState((state: State) => state.todos.items);
* return todos.map(todo => );
* }
*/
export function useStoreState<
StoreState extends State = State<{}>,
Result = any
>(
mapState: (state: StoreState) => Result,
equalityFn?: (prev: Result, next: Result) => boolean,
): Result;
/**
* A React Hook allowing you to use actions within your component.
*
* https://easy-peasy.now.sh/docs/api/use-store-actions.html
*
* Note: you can create a pre-typed version of this hook via "createTypedHooks"
*
* @example
*
* import { useStoreActions, Actions } from 'easy-peasy';
*
* function MyComponent() {
* const addTodo = useStoreActions((actions: Actions) => actions.todos.add);
* return ;
* }
*/
export function useStoreActions<
StoreActions extends Actions = Actions<{}>,
Result = any
>(mapActions: (actions: StoreActions) => Result): Result;
/**
* A react hook that returns the store instance.
*
* https://easy-peasy.now.sh/docs/api/use-store.html
*
* Note: you can create a pre-typed version of this hook via "createTypedHooks"
*
* @example
*
* import { useStore } from 'easy-peasy';
*
* function MyComponent() {
* const store = useStore();
* return {store.getState().foo}
;
* }
*/
export function useStore<
StoreModel extends object = {},
StoreConfig extends EasyPeasyConfig = EasyPeasyConfig<
undefined,
any
>
>(): Store;
/**
* A React Hook allowing you to use the store's dispatch within your component.
*
* https://easypeasy.now.sh/docs/api/use-store-dispatch.html
*
* Note: you can create a pre-typed version of this hook via "createTypedHooks"
*
* @example
*
* import { useStoreDispatch } from 'easy-peasy';
*
* function MyComponent() {
* const dispatch = useStoreDispatch();
* return dispatch({ type: 'ADD_TODO', payload: todo })} />;
* }
*/
export function useStoreDispatch(): Dispatch<
StoreModel
>;
/**
* A utility function used to create pre-typed hooks.
*
* https://easypeasy.now.sh/docs/api/create-typed-hooks.html
*
* @example
* import { StoreModel } from './store';
*
* const { useStoreActions, useStoreState, useStoreDispatch, useStore } = createTypedHooks();
*
* useStoreActions(actions => actions.todo.add); // fully typed
*/
export function createTypedHooks(): {
useStoreActions: (
mapActions: (actions: Actions) => Result,
) => Result;
useStoreDispatch: () => Dispatch;
useStoreState: (
mapState: (state: State) => Result,
equalityFn?: (prev: Result, next: Result) => boolean,
) => Result;
useStore: () => Store;
};
// #endregion
// #region StoreProvider
/**
* Exposes the store to your app (and hooks).
*
* https://easypeasy.now.sh/docs/api/store-provider.html
*
* @example
*
* import { StoreProvider } from 'easy-peasy';
* import store from './store';
*
* ReactDOM.render(
*
*
*
* );
*/
export class StoreProvider extends Component<{
store: Store;
}> {}
// #endregion
// #region Context + Local Stores
interface StoreModelInitializer<
StoreModel extends object,
InitialData extends undefined | object
> {
(initialData?: InitialData): StoreModel;
}
export function createContextStore<
StoreModel extends object = {},
InitialData extends undefined | object = undefined,
StoreConfig extends EasyPeasyConfig = EasyPeasyConfig<{}, any>
>(
model: StoreModel | StoreModelInitializer,
config?: StoreConfig,
): {
Provider: React.SFC<{ initialData?: InitialData }>;
useStore: () => Store;
useStoreState: (
mapState: (state: State) => Result,
dependencies?: Array,
) => Result;
useStoreActions: (
mapActions: (actions: Actions