import { Component } from 'react'; import { UnknownAction, compose, Dispatch as ReduxDispatch, Middleware, Observable, Reducer as ReduxReducer, Store as ReduxStore, StoreEnhancer, } from 'redux'; import { O } from 'ts-toolbelt'; export type ReduxAction = UnknownAction; /** * 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 = | [] | Array>; type AnyFunction = (...args: any[]) => any; type ActionEmitterTypes = Action | Thunk; type ActionListenerTypes = ActionOn | ThunkOn; type ActionTypes = | ActionEmitterTypes | ActionListenerTypes | EffectOn; interface ActionCreator { (payload: Payload): void; type: string; z__creator: 'actionWithPayload'; } interface ThunkCreator { (payload: Payload extends undefined ? void : Payload): Result; type: string; startType: string; successType: string; failType: string; z__creator: 'thunkWithPayload'; } type ActionOrThunkCreator = | ActionCreator | ThunkCreator; type Helpers = { dispatch: Dispatch; fail: AnyFunction; getState: () => State; getStoreActions: () => Actions; getStoreState: () => State; injections: Injections; meta: Meta; }; // #region Helpers /** * This utility will pull the state within an action out of the Proxy form into * a natural form, allowing you to console.log or inspect it. * * @param state - The action state * * @example * * ```typescript * import { debug, action } from 'easy-peasy'; * * const model = { * todos: [], * addTodo: action((state, payload) => { * console.log(debug(state)); * state.todos.push(payload); * console.log(debug(state)); * }) * } * ``` */ export function debug( state: StateDraft, ): StateDraft; // #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 listener 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 * * ```typescript * import { Actions, useStoreActions } from 'easy-peasy'; * import { StoreModel } from './my-store'; * * function MyComponent() { * const doSomething = useStoreActions( * (actions: Actions) => actions.doSomething * ); * } * ``` */ 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 FilterActionTypes = { [K in keyof Model as Model[K] extends ActionTypes ? never : K]: Model[K]; }; type RecursiveState = StateMapper< FilterActionTypes >; /** * Filters a model into a type that represents the state only (i.e. no actions) * * @example * * ```typescript * import { State, useStoreState } from 'easy-peasy'; * import { StoreModel } from './my-store'; * * function MyComponent() { * const stuff = useStoreState((state: State) => state.stuff); * } * ``` */ export type State = RecursiveState; // #endregion // #region Store + Config + Creation /** * Creates a store. * * https://easy-peasy.dev/docs/api/create-store.html * * @example * * ```typescript * import { createStore } from 'easy-peasy'; * * interface StoreModel { * todos: string[]; * } * * const store = createStore({ * todos: [] * }); * ``` */ export function createStore< StoreModel extends object = {}, InitialState extends undefined | object = undefined, Injections extends object = {}, >( model: StoreModel, config?: EasyPeasyConfig, ): Store>; /** * Configuration interface for stores. * * @example * * ```typescript * import { createStore } from 'easy-peasy'; * import model from './my-model'; * * const store = createStore(model, { * devTools: false, * name: 'MyConfiguredStore', * }); * ``` */ export interface EasyPeasyConfig< InitialState extends undefined | object = undefined, Injections extends object = {}, > { compose?: typeof compose; devTools?: boolean | object; disableImmer?: boolean; enhancers?: StoreEnhancer[]; initialState?: InitialState; injections?: Injections; middleware?: Array>; mockActions?: boolean; name?: string; version?: number; reducerEnhancer?: (reducer: ReduxReducer) => ReduxReducer; } export interface MockedAction { type: string; [key: string]: any; } export interface AddModelResult { resolveRehydration: () => Promise; } /** * An Easy Peasy store. This is essentially a Redux store with additional enhanced * APIs attached. * * @example * * ```typescript * import { Store } from 'easy-peasy'; * import { StoreModel } from './store'; * * type MyEasyPeasyStore = Store; * ``` */ export interface Store< StoreModel extends object = {}, StoreConfig extends EasyPeasyConfig = EasyPeasyConfig< undefined, {} >, > 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 = UnknownAction, > = 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[]; }; /** * Declares a thunk against your model type definition. * * https://easy-peasy.dev/docs/typescript-api/thunk.html * * @param Model - The model that the thunk is being bound to. * @param Payload - The type of the payload expected. Set to undefined if none. * @param Injections - The type for the injections provided to the store * @param StoreModel - The root model type for the store. Useful if using getStoreState helper. * @param Result - The type for the expected return from the thunk. * * @example * * import { Thunk } from 'easy-peasy'; * * interface TodosModel { * todos: Array; * addTodo: Thunk; * } */ export interface Thunk< Model extends object, Payload = undefined, Injections = any, StoreModel extends object = {}, Result = any, > { type: 'thunk'; payload: Payload; result: Result; } /** * Declares an thunk against your model. * * Thunks are typically used to encapsulate side effects and are able to * dispatch other actions. * * https://easy-peasy.dev/docs/api/thunk.html * * @example * * ```typescript * 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 = undefined, Injections = any, StoreModel extends object = {}, Result = any, >( thunk: ( actions: Actions, payload: Payload, helpers: Helpers, ) => 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; }; /** * @param {boolean} [immer=true] - If true, the action will be wrapped in an immer produce call. Otherwise, the action will update the state directly. **/ interface Config { immer?: boolean; } /** * Declares an action. * * https://easy-peasy.dev/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, config?: Config, ): 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, config?: Config, ): ActionOn; // #endregion // #region Computed /** * Represents a computed property. * * @example * * import { Computed } from 'easy-peasy'; * * interface Model { * products: Array; * totalPrice: Computed; * } */ export interface Computed< Model extends object, Result, StoreModel extends object = {}, > { type: 'computed'; result: Result; } type DefaultComputationFunc = ( state: State, ) => Result; type ExtractReturnTypes any)[]> = [ ...{ [K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : never; }, ]; export function computed< Model extends object = {}, Result = void, StoreModel extends object = {}, Resolvers extends StateResolvers = StateResolvers< Model, StoreModel >, >( resolversOrCompFunc: Resolvers | DefaultComputationFunc, compFunc?: (...args: ExtractReturnTypes) => Result, ): Computed; // #endregion // #region EffectOn export interface EffectOn< Model extends object = {}, StoreModel extends object = {}, Injections = any, > { type: 'effectOn'; } type Change> = { prev: ExtractReturnTypes; current: ExtractReturnTypes; action: { type: string; payload?: any; }; }; export type Dispose = () => any; export function 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 | void | Dispose | Promise, ): 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( reducer: ReduxReducer, config?: Config, ): 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.dev/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.dev/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.dev/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, {} >, >(): 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< StoreModel extends object = {}, >(): Dispatch; /** * 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; children?: React.ReactNode; }> {} // #endregion // #region Context + Local Stores interface StoreModelInitializer< StoreModel extends object, RuntimeModel extends undefined | object, > { (runtimeModel?: RuntimeModel): StoreModel; } export function createContextStore< StoreModel extends object = {}, Injections extends object = {}, RuntimeModel extends undefined | object = StoreModel, StoreConfig extends EasyPeasyConfig = EasyPeasyConfig< undefined, Injections >, >( model: StoreModel | StoreModelInitializer, config?: StoreConfig, ): { Provider: React.FC<{ children?: React.ReactNode; runtimeModel?: RuntimeModel; injections?: Injections | ((previousInjections: Injections) => Injections); }>; useStore: () => Store; useStoreState: ( mapState: (state: State) => Result, equalityFn?: (prev: Result, next: Result) => boolean, ) => Result; useStoreActions: ( mapActions: (actions: Actions) => Result, ) => Result; useStoreDispatch: () => Dispatch; useStoreRehydrated: () => boolean; }; export function useLocalStore< StoreModel extends object = {}, StoreConfig extends EasyPeasyConfig = EasyPeasyConfig< undefined, {} >, >( 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, fullState?: any) => any; out?: (data: any, key: string, fullState?: any) => any; } export interface PersistConfig { allow?: Array; deny?: Array; mergeStrategy?: 'mergeDeep' | 'mergeShallow' | 'overwrite'; migrations?: { migrationVersion: number; [key: number]: ( state: Partial, ) => void; }; storage?: 'localStorage' | 'sessionStorage' | PersistStorage; transformers?: Array; } export interface TransformConfig { blacklist?: Array; whitelist?: Array; } export function createTransform( inbound?: (data: any, key: string, fullState?: any) => any, outbound?: (data: any, key: string, fullState?: any) => any, config?: TransformConfig, ): Transformer; export function persist( model: Model, config?: PersistConfig, ): Model; export function useStoreRehydrated(): boolean; // #endregion