/// 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) => Result, ) => Result; useStoreDispatch: () => Dispatch; useStoreRehydrated: () => boolean; }; interface UseLocalStore< StoreModel extends object, InitialData extends undefined | object > { (initialData?: InitialData): [State, Actions]; } export function createComponentStore< StoreModel extends object = {}, InitialData extends undefined | object = undefined, StoreConfig extends EasyPeasyConfig = EasyPeasyConfig<{}, any> >( model: StoreModel | StoreModelInitializer, config?: StoreConfig, ): UseLocalStore; export function useLocalStore< StoreModel extends object = {}, StoreConfig extends EasyPeasyConfig = EasyPeasyConfig<{}, any> >( modelCreator: (prevState?: State) => StoreModel, dependencies?: any[], storeConfig?: ( prevState?: State, prevConfig?: StoreConfig, ) => StoreConfig, ): [State, Actions, Store]; // #endregion // #region Persist export interface PersistStorage { getItem: (key: string) => any | Promise; setItem: (key: string, data: any) => void | Promise; removeItem: (key: string) => void | Promise; } export interface Transformer { in?: (data: any, key: string) => any; out?: (data: any, key: string) => any; } export interface PersistConfig { blacklist?: Array; mergeStrategy?: 'merge' | 'mergeDeep' | 'overwrite'; storage?: 'localStorage' | 'sessionStorage' | PersistStorage; transformers?: Array; whitelist?: Array; } export interface TransformConfig { blacklist?: Array; whitelist?: Array; } export function createTransform( inbound?: (data: any, key: string) => any, outbound?: (data: any, key: string) => any, config?: TransformConfig, ): Transformer; export function persist( model: Model, config?: PersistConfig, ): Model; export function useStoreRehydrated(): boolean; // #endregion