typescriptreduxredux-observabletypesafe-actions

How to create generic async request epics that uses typesafe-actions AsyncCreator to simplify handling api request flows in Redux


I am a newbie struggling to understand the best architecture for relating epics to async actions in Typescript redux environment....

Is it possible to create a generic async request epic for a given typesafe-actions AsyncCreator instance?

I have had a try with the code below but lose type information on the action payload....only the type property is recognised on the filtered action....The transpilation error that I receive is...

Property 'payload' does not exist on type 'ReturnType<[TRequestPayload] extends [undefined] ? unknown extends TRequestPayload ? PayloadAC<TRequestType, TRequestPayload> : EmptyAC<TRequestType> : PayloadAC<TRequestType, TRequestPayload>>'.
const createAsyncEpic = <
  TModel, // generic type for data payload
  TRequestType extends string,
  TRequestPayload extends ApiRequest<TModel>,
  TSuccessType extends string,
  TSuccessPayload extends ApiSuccess<TModel>,
  TFailureType extends string,
  TFailurePayload extends ApiFailure<TModel>
>(
  asyncAction: AsyncActionCreator<
    [TRequestType, TRequestPayload],
    [TSuccessType, TSuccessPayload],
    [TFailureType, TFailurePayload]
  >,
) => {
  const epic: Epic<RootAction, RootAction, RootState, Services> = (
    action$,
    state$,
    { apiService },
  ) =>
    action$.pipe(
      filter(isActionOf(asyncAction.request)),
      switchMap(action =>
        apiService
          .api()
          .all<TModel>(action.payload.url) // property payload unrecognised, only type property recognised here
          .pipe(
            map(response => asyncAction.success(response.data)),
            catchError(error =>
              of(
                asyncAction.failure(error),
              ),
            ),
          ),
      ),
    );
  return epic;
};

Has anybody achieved this as an alternative to creating an epic for each individual api action type per resource? For example creating an epic explicitly for each flow:

  1. [FETCH_TODO_REQUEST, FETCH_TODO_SUCCESS, FETCH_TODO_FAILURE],
  2. [POST_TODO_REQUEST, POST_TODO_SUCCESS, POST_TODO_FAILURE].......,
  3. [FETCH_POST_REQUEST, FETCH_POST_SUCCESS, FETCH_POST_FAILURE],
  4. [PUT_POST_REQUEST, PUT_POST_SUCCESS, PUT_POST_FAILURE]........, etc.

Updated information

Slightly closer.... modified method function signature to accept data model type.

const createAsyncEpic = <
  TModel // generic type for data payload model
>(
  asyncAction: AsyncActionCreator<
    [string, ApiRequest<TModel>],
    [string, ApiSuccess<TModel | TModel[]>],
    [string, ApiFailure<TModel>]
  >,
) => {
  const epic: Epic<RootAction, RootAction, RootState, Services> = (
    action$,
    state$,
    { courseServices, apiService },
  ) =>
    action$.pipe(
      filter(isActionOf(asyncAction.request)),
      switchMap(action =>
        apiService
          .api()
          .all<TModel>(action.payload.url)
          .pipe(
            map(resp =>
              asyncAction.success({
                request: action.payload, // Api request struct
                response: resp, // Axios Response
              }),
            ),
            catchError(error =>
              of(asyncAction.failure(action.payload.fail(error))),
            ),
          ),
      ),
    );
  return epic;
};

Now getting a different error:

ERROR in <path>/epics.ts
[tsl] ERROR in <path>/epics.ts(42,5)
      TS2322: Type 'Observable<PayloadAction<TSuccessType, ApiSuccess<TModel | TModel[]>>>' is not assignable to type 'Observable<PayloadAction<actions.FETCH_COURSES_REQUEST, RequestingComponent> | PayloadAction<actions.FETCH_COURSES_SUCCESS, Course[]> | PayloadAction<constants.NOTIFY_ERROR, ErrorReport> | PayloadAction<string, ApiSuccess<Course>> | { ...; } | { ...; } | PayloadAction<...> | PayloadAction<...>>'.
  Type 'PayloadAction<TSuccessType, ApiSuccess<TModel | TModel[]>>' is not assignable to type 'PayloadAction<actions.FETCH_COURSES_REQUEST, RequestingComponent> | PayloadAction<actions.FETCH_COURSES_SUCCESS, Course[]> | PayloadAction<constants.NOTIFY_ERROR, ErrorReport> | PayloadAction<string, ApiSuccess<Course>> | { ...; } | { ...; } | PayloadAction<...> | PayloadAction<...>'.
    Type 'PayloadAction<TSuccessType, ApiSuccess<TModel | TModel[]>>' is not assignable to type 'PayloadAction<actions.FETCH_COURSES_REQUEST, RequestingComponent>'.
      Type 'TSuccessType' is not assignable to type 'actions.FETCH_COURSES_REQUEST'.
        Type 'string' is not assignable to type 'actions.FETCH_COURSES_REQUEST'.

Is it something to do with looking to match generic action used in the epic with RootAction types in Epic type declaration???

export declare interface Epic<Input extends Action = any, Output extends Input = Input, State = any, Dependencies = any> {
  (action$: ActionsObservable<Input>, state$: StateObservable<State>, dependencies: Dependencies): Observable<Output>;
}

Solution

  • Solved it and managed to get it compiling. It was a type declarations issue.

    When using Typescript, the action types declared in the epic signature must be a subset of all action types that were declared when the middleware was setup.

    I had not instantiated an instance of new generic action and I had also not added it as a type to the RootAction(union of all action types). The RootAction still also contained some old action types from older architecture before refactoring.

    The code compiled after doing this and refactoring my epic signature to:

    I now have an epic for generically retrieving a list of resources from an api. Other epics can follow for patch, post, put operations etc.