easy-peasy
Version:
Vegetarian friendly state for React
843 lines (747 loc) • 19.7 kB
TypeScript
/// <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