UNPKG

easy-peasy

Version:

Vegetarian friendly state for React

843 lines (747 loc) 19.7 kB
/// <reference types="symbol-observable" /> /* eslint-disable */ import { Component } from 'react'; import { compose, AnyAction, Action as ReduxAction, Dispatch as ReduxDispatch, Reducer as ReduxReducer, Store as ReduxStore, StoreEnhancer, Middleware, Observable, } from 'redux'; import { O } from 'ts-toolbelt'; /** * Picks only the keys of a certain type */ type KeysOfType<A extends object, B> = { [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<A extends Object> = { [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 ActionTypes = | Action<any, any> | Thunk<any, any, any, any, any> | ActionOn<any, any> | ThunkOn<any, any, any>; interface ActionCreator<Payload> { (payload: Payload): void; type: string; z__creator: 'actionWithPayload'; } interface ThunkCreator<Payload, Result> { (payload: Payload): Result; type: string; startType: string; successType: string; failType: string; z__creator: 'thunkWithPayload'; } type ActionOrThunkCreator<Payload = void, Result = void> = | ActionCreator<Payload> | ThunkCreator<Payload, Result>; // #region Helpers export function debug<StateDraft extends any>(state: StateDraft): StateDraft; export function memo<Fn extends Function = any>(fn: Fn, cacheSize: number): Fn; // #endregion // #region Listeners type ListenerMapper<ActionsModel extends object, Depth extends string> = { [P in keyof ActionsModel]: ActionsModel[P] extends ActionOn<any, any> ? ActionCreator<TargetPayload<any>> : ActionsModel[P] extends ThunkOn<any, any, any> ? ThunkCreator<TargetPayload<any>, any> : ActionsModel[P] extends object ? RecursiveListeners< ActionsModel[P], Depth extends '1' ? '2' : Depth extends '2' ? '3' : Depth extends '3' ? '4' : Depth extends '4' ? '5' : '6' > : unknown; }; type RecursiveListeners< Model extends object, Depth extends string > = Depth extends '6' ? Model : ListenerMapper< O.Filter< O.Select<Model, object>, | Array<any> | RegExp | Date | string | Reducer<any, any> | Computed<any, any, any> | Action<any, any> | Thunk<any, any, any, any, any> >, Depth >; /** * Filters a model into a type that represents the listeners action creators * * @example * * type OnlyActions = Actions<Model>; */ export type Listeners<Model extends object = {}> = RecursiveListeners< Model, '1' >; // #endregion // #region Actions type ActionMapper<ActionsModel extends object, Depth extends string> = { [P in keyof ActionsModel]: ActionsModel[P] extends Action<any, any> ? ActionCreator<ActionsModel[P]['payload']> : ActionsModel[P] extends Thunk<any, any, any, any, any> ? ActionsModel[P]['payload'] extends void ? ThunkCreator<void, ActionsModel[P]['result']> : ThunkCreator<ActionsModel[P]['payload'], ActionsModel[P]['result']> : ActionsModel[P] extends object ? RecursiveActions< ActionsModel[P], Depth extends '1' ? '2' : Depth extends '2' ? '3' : Depth extends '3' ? '4' : Depth extends '4' ? '5' : '6' > : unknown; }; type RecursiveActions< Model extends object, Depth extends string > = Depth extends '6' ? Model : ActionMapper< O.Filter< O.Select<Model, object>, | Array<any> | RegExp | Date | string | Reducer<any, any> | Computed<any, any, any> | ActionOn<any, any> | ThunkOn<any, any, any> >, Depth >; /** * Filters a model into a type that represents the action/thunk creators * * @example * * type OnlyActions = Actions<Model>; */ export type Actions<Model extends object = {}> = RecursiveActions<Model, '1'>; // #endregion // #region State type StateMapper<StateModel extends object, Depth extends string> = { [P in keyof StateModel]: P extends IndexSignatureKeysOfType<StateModel> ? StateModel[P] : StateModel[P] extends Computed<any, any, any> ? StateModel[P]['result'] : StateModel[P] extends Reducer<any, any> ? StateModel[P]['result'] : StateModel[P] extends object ? StateModel[P] extends string | Array<any> | RegExp | Date | Function ? StateModel[P] : RecursiveState< StateModel[P], Depth extends '1' ? '2' : Depth extends '2' ? '3' : Depth extends '3' ? '4' : Depth extends '4' ? '5' : '6' > : StateModel[P]; }; type RecursiveState< Model extends object, Depth extends string > = Depth extends '6' ? Model : StateMapper<O.Filter<Model, ActionTypes>, Depth>; /** * Filters a model into a type that represents the state only (i.e. no actions) * * @example * * type StateOnly = State<Model>; */ export type State<Model extends object = {}> = RecursiveState<Model, '1'>; // #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<string>; * } * } * * const store = createStore<StoreModel>({ * todos: { * items: [], * } * }) */ export function createStore< StoreModel extends Object = {}, InitialState extends object = {}, Injections = any >( model: StoreModel, config?: EasyPeasyConfig<InitialState, Injections>, ): Store<StoreModel, EasyPeasyConfig<InitialState, Injections>>; /** * Configuration interface for the createStore */ export interface EasyPeasyConfig< InitialState extends Object = {}, Injections = any > { compose?: typeof compose; devTools?: boolean; disableImmer?: boolean; enhancers?: StoreEnhancer[]; initialState?: InitialState; injections?: Injections; middleware?: Array<Middleware<any, any, any>>; mockActions?: boolean; name?: string; reducerEnhancer?: (reducer: ReduxReducer<any, any>) => ReduxReducer<any, any>; } export interface MockedAction { type: string; [key: string]: any; } /** * Represents a Redux store, enhanced by easy peasy. * * @example * * type EnhancedReduxStore = Store<StoreModel>; */ export interface Store< StoreModel extends object = {}, StoreConfig extends EasyPeasyConfig<any, any> = any > extends ReduxStore<State<StoreModel>> { addModel: <ModelSlice extends object>( key: string, modelSlice: ModelSlice, ) => void; clearMockedActions: () => void; dispatch: Dispatch<StoreModel>; getActions: () => Actions<StoreModel>; getListeners: () => Listeners<StoreModel>; getMockedActions: () => MockedAction[]; reconfigure: <NewStoreModel extends object>(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<State<StoreModel>>; } // #endregion // #region Dispatch /** * Enhanced version of the Redux Dispatch with action creators bound to it * * @example * * type DispatchWithActions = Dispatch<StoreModel>; */ export type Dispatch< StoreModel extends object = {}, Action extends ReduxAction = ReduxAction<any> > = Actions<StoreModel> & ReduxDispatch<Action>; // #endregion // #region Types shared by ActionOn and ThunkOn type Target = ActionOrThunkCreator<any> | string; type TargetResolver<Model extends object, StoreModel extends object> = ( actions: Actions<Model>, storeActions: Actions<StoreModel>, ) => Target | Array<Target>; interface TargetPayload<Payload> { type: string; payload: Payload; result?: any; error?: Error; resolvedTargets: Array<string>; } type PayloadFromResolver< Resolver extends TargetResolver<any, any>, Resolved = ReturnType<Resolver> > = Resolved extends string ? any : Resolved extends ActionOrThunkCreator<infer Payload> ? Payload : Resolved extends Array<infer T> ? T extends string ? any : T extends ActionOrThunkCreator<infer Payload> ? 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<string>; * addTodo: Thunk<TodosModel, string>; * } */ 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<Model>, payload: Payload, helpers: { dispatch: Dispatch<StoreModel>; getState: () => State<Model>; getStoreActions: () => Actions<StoreModel>; getStoreState: () => State<StoreModel>; injections: Injections; meta: Meta; }, ) => Result, ): Thunk<Model, Payload, Injections, StoreModel, Result>; // #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<Model, StoreModel> = TargetResolver< Model, StoreModel > >( targetResolver: Resolver, handler: ( actions: Actions<Model>, target: TargetPayload<PayloadFromResolver<Resolver>>, helpers: { dispatch: Dispatch<StoreModel>; getState: () => State<Model>; getStoreActions: () => Actions<StoreModel>; getStoreState: () => State<StoreModel>; injections: Injections; meta: Meta; }, ) => any, ): ThunkOn<Model, Injections, StoreModel>; // #endregion // #region Action /** * Represents an action. * * @example * * import { Action } from 'easy-peasy'; * * interface Model { * todos: Array<Todo>; * addTodo: Action<Model, Todo>; * } */ export type Action<Model extends object = {}, Payload = void> = { type: 'action'; payload: Payload; result: void | State<Model>; }; /** * 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<Model extends object = {}, Payload = any>( action: (state: State<Model>, payload: Payload) => void | State<Model>, ): Action<Model, Payload>; // #endregion // #region Listener Action export interface ActionOn< Model extends object = {}, StoreModel extends object = {} > { type: 'actionOn'; result: void | State<Model>; } export function actionOn< Model extends object, StoreModel extends object, Resolver extends TargetResolver<Model, StoreModel> >( targetResolver: Resolver, handler: ( state: State<Model>, target: TargetPayload<PayloadFromResolver<Resolver>>, ) => void | State<Model>, ): ActionOn<Model, StoreModel>; // #endregion // #region Computed /** * Represents a computed property. * * @example * * import { Computed } from 'easy-peasy'; * * interface Model { * products: Array<Product>; * totalPrice: Computed<Model, number>; * } */ export type Computed< Model extends object = {}, Result = any, StoreModel extends object = {} > = { type: 'computed'; result: Result; }; type Resolver<Model extends object, StoreModel extends object> = ( state: State<Model>, storeState: State<StoreModel>, ) => any; type DefaultComputationFunc<Model extends object, Result> = ( state: State<Model>, ) => Result; export function computed< Model extends object, Result, StoreModel extends object, Resolvers extends Resolver<Model, StoreModel>[] >( resolversOrCompFunc: (Resolvers | []) | DefaultComputationFunc<Model, Result>, compFunc?: ( ...args: { [K in keyof Resolvers]: Resolvers[K] extends (...args: any[]) => any ? ReturnType<Resolvers[K]> : string; } ) => Result, ): Computed<Model, Result, StoreModel>; // #endregion // #region Reducer /** * A reducer type. * * Useful when declaring your model. * * @example * * import { Reducer } from 'easy-peasy'; * * interface Model { * router: Reducer<ReactRouterState>; * } */ export type Reducer<State = any, Action extends ReduxAction = AnyAction> = { 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>(state: ReduxReducer<State>): Reducer<State>; // #endregion // #region Hooks /** * A React Hook allowing you to use state within your component. * * https://github.com/ctrlplusb/easy-peasy#usestoremapstate-externals * * @example * * import { useStoreState, State } from 'easy-peasy'; * * function MyComponent() { * const todos = useStoreState((state: State<StoreModel>) => state.todos.items); * return todos.map(todo => <Todo todo={todo} />); * } */ export function useStoreState<StoreState extends State<any>, Result>( mapState: (state: StoreState) => Result, ): Result; /** * A React Hook allowing you to use actions within your component. * * https://github.com/ctrlplusb/easy-peasy#useactionsmapactions * * @example * * import { useStoreActions, Actions } from 'easy-peasy'; * * function MyComponent() { * const addTodo = useStoreActions((actions: Actions<StoreModel>) => actions.todos.add); * return <AddTodoForm save={addTodo} />; * } */ export function useStoreActions<StoreActions extends Actions<any>, Result>( mapActions: (actions: StoreActions) => Result, ): Result; /** * A react hook that returns the store instance. * * @example * * import { useStore } from 'easy-peasy'; * * function MyComponent() { * const store = useStore(); * return <div>{store.getState().foo}</div>; * } */ export function useStore< StoreModel extends object = {}, StoreConfig extends EasyPeasyConfig<any, any> = any >(): Store<StoreModel, StoreConfig>; /** * A React Hook allowing you to use the store's dispatch within your component. * * https://github.com/ctrlplusb/easy-peasy#usedispatch * * @example * * import { useStoreDispatch } from 'easy-peasy'; * * function MyComponent() { * const dispatch = useStoreDispatch(); * return <AddTodoForm save={(todo) => dispatch({ type: 'ADD_TODO', payload: todo })} />; * } */ export function useStoreDispatch<StoreModel extends object = {}>(): Dispatch< StoreModel >; /** * A utility function used to create pre-typed hooks. * * @example * const { useStoreActions, useStoreState, useStoreDispatch, useStore } = createTypedHooks<StoreModel>(); * * useStoreActions(actions => actions.todo.add); // fully typed */ export function createTypedHooks<StoreModel extends Object = {}>(): { useStoreActions: <Result>( mapActions: (actions: Actions<StoreModel>) => Result, ) => Result; useStoreDispatch: () => Dispatch<StoreModel>; useStoreState: <Result>( mapState: (state: State<StoreModel>) => Result, dependencies?: Array<any>, ) => Result; useStore: () => Store<StoreModel>; }; // #endregion // #region StoreProvider /** * Exposes the store to your app (and hooks). * * https://github.com/ctrlplusb/easy-peasy#storeprovider * * @example * * import { StoreProvider } from 'easy-peasy'; * * ReactDOM.render( * <StoreProvider store={store}> * <App /> * </StoreProvider> * ); */ export class StoreProvider<StoreModel extends object = {}> extends Component<{ store: Store<StoreModel>; }> {} // #endregion // #region Context + Local Stores interface StoreModelInitializer< StoreModel extends object = {}, InitialData = any > { (initialData?: InitialData): StoreModel; } export function createContextStore< StoreModel extends object = {}, InitialData = any, StoreConfig extends EasyPeasyConfig<any, any> = any >( model: StoreModel | StoreModelInitializer<StoreModel, InitialData>, config?: StoreConfig, ): { Provider: React.SFC<{ initialData?: InitialData }>; useStore: () => Store<StoreModel, StoreConfig>; useStoreState: <Result = any>( mapState: (state: State<StoreModel>) => Result, dependencies?: Array<any>, ) => Result; useStoreActions: <Result = any>( mapActions: (actions: Actions<StoreModel>) => Result, ) => Result; useStoreDispatch: () => Dispatch<StoreModel>; useStoreRehydrated: () => boolean; }; interface UseLocalStore<StoreModel extends object, InitialData> { (initialData?: InitialData): [State<StoreModel>, Actions<StoreModel>]; } export function createComponentStore< StoreModel extends object = {}, InitialData = any, StoreConfig extends EasyPeasyConfig<any, any> = any >( model: StoreModel | StoreModelInitializer<StoreModel, InitialData>, config?: StoreConfig, ): UseLocalStore<StoreModel, InitialData>; // #endregion // #region Persist export interface PersistStorage { getItem: (key: string) => any | Promise<any>; setItem: (key: string, data: any) => void | Promise<void>; removeItem: (key: string) => void | Promise<void>; } export interface Transformer { in?: (data: any, key: string) => any; out?: (data: any, key: string) => any; } export interface PersistConfig<Model extends object> { blacklist?: Array<keyof Model>; mergeStrategy?: 'merge' | 'mergeDeep' | 'overwrite'; storage?: 'localStorage' | 'sessionStorage' | PersistStorage; transformers?: Array<Transformer>; whitelist?: Array<keyof Model>; } export interface TransformConfig { blacklist?: Array<string>; whitelist?: Array<string>; } export function createTransform( inbound?: (data: any, key: string) => any, outbound?: (data: any, key: string) => any, config?: TransformConfig, ): Transformer; export function persist<Model extends object>( model: Model, config?: PersistConfig<Model>, ): Model; export function useStoreRehydrated(): boolean; // #endregion