3

I'm setting TypeScript and Redux-Observable in my app and have this simple flow I'd like to apply here with an Epic:

dispatch('someAction')
fetch('relevant-endpoint-from-api')
getResponse(dispatch('someActionSuccess'))
failed(dispatch('someActionFailed'))

So following this article that is just doing that, I gave it a try.

Here's my actions.ts file:

export interface MediasOrigin {
  id: number;
  label: string;
  url: string;
  zonesId: number[];
  scenariosRef: number[];
  stepsRef: number[];
  gearsId: number[];
  texturesId: number[];
}

export enum MediasActionTypes {
  MEDIAS_ORIGINS_FETCH_ALL = "MEDIAS_ORIGINS_FETCH_ALL",
  MEDIAS_ORIGINS_FETCH_ALL_SUCCESS = "MEDIAS_ORIGINS_FETCH_ALL_SUCCESS",
  MEDIAS_ORIGINS_FETCH_ALL_FAILED = "MEDIAS_ORIGINS_FETCH_ALL_FAILED"
}

export interface IMediasOriginsFetchAllAction {
  type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL;
}

export interface IMediasOriginsFetchAllSuccessAction {
  type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL;
  payload: {
    medias: MediasOrigin[]
  }
}

export interface IMediasOriginsFetchAllFailedAction {
  type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL;
  payload: {
    error: string
  }
}

/* ACTION CREATOR */
export function fetchAllMediaOrigins(): IMediasOriginsFetchAllAction {
  return {
    type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL
  }
}

export type MediasAction = IMediasOriginsFetchAllAction | IMediasOriginsFetchAllSuccessAction | IMediasOriginsFetchAllFailedAction

And then my epic.ts file:

import { from, Observable, of } from "rxjs"
import { map, catchError, switchMap, filter } from "rxjs/operators"
import { isOfType } from "typesafe-actions"
import { Epic } from "redux-observable"

import ApiClient from "agent" // change, wrapper around superagent (http client)
import {IStore} from "reducers"
import {
  MediasAction,
  MediasActionTypes,
  IMediasOrigin
} from "./actions"

const getAllMediasOriginEpic: Epic<MediasAction, MediasAction, IStore> = action$ => {
  const request = new ApiClient();

  return action$.pipe(
    filter(isOfType(MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL)),
    switchMap(action => // change
      from(request.getAllOriginMedias()).pipe( // change
        map((response: IMediasOrigin[]) // change =>
          of({
            type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL_SUCCESS,
            payload: { medias: response },
          })
        ),
        catchError((error: Error) // change =>
          of({
            type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL_FAILED,
            payload: {error: Error.message}, // change
          })
        )
      )
    )
  )
}

export default combineEpics(getAllMediasOriginEpic)

I'm new to TypeScript so maybe the error seems obvious to aficionados, but I don't get what I've done wrong based on TypeScript's message.

(no longer relevant) The message points at the action$ returned in the epic:

Type 'Observable<IMediasOriginsFetchAllAction | Observable<{ type: MediasActionTypes; payload: { medias: unknown; }; }>>' is not assignable to type 'Observable<MediasAction>'.
  Type 'IMediasOriginsFetchAllAction | Observable<{ type: MediasActionTypes; payload: { medias: unknown; }; }>' is not assignable to type 'MediasAction'.
    Type 'Observable<{ type: MediasActionTypes; payload: { medias: unknown; }; }>' is not assignable to type 'MediasAction'.
      Type 'Observable<{ type: MediasActionTypes; payload: { medias: unknown; }; }>' is missing the following properties from type 'IMediasOriginsFetchAllSuccessAction': type, payload  TS2322

I don't get what I could do to fix this, from the message it seems that all I've done is wrong and that medias is unknown although it's specified as being an array of MediasOrigin in the interface. Am I missing something here?

For information, here are the packages versions:

  • typescript: 4.0.2
  • redux-observable: 1.2.0
  • rxjs: 6.6.3

// Edit: 09/24/2020

I made a couple changes in order to make it simpler and got why the error message was talking about a mediasUnknown: I wasn't telling in the epic that response mapped from the api call was of MediasOrigin[] (renamed IMediasOrigin[] now). Also I removed the api method that was a custom operator of my own. Changes have been brought to the scripts above and signaled with a // change comment.

Here's now the error message:

TypeScript error in /Users/utopiad/Dev/spectral-core/platform/src/components/Medias/epics.ts(11,7):
Type '(action$: ActionsObservable<MediasAction>) => Observable<Observable<{ type: MediasActionTypes; payload: { medias: IMediasOrigin[]; }; }> | { ...; }>' is not assignable to type 'Epic<MediasAction, MediasAction, IStore, any>'.
  Type 'Observable<Observable<{ type: MediasActionTypes; payload: { medias: IMediasOrigin[]; }; }> | { type: MediasActionTypes; payload: { ...; }; }>' is not assignable to type 'Observable<MediasAction>'.
    Type 'Observable<{ type: MediasActionTypes; payload: { medias: IMediasOrigin[]; }; }> | { type: MediasActionTypes; payload: { ...; }; }' is not assignable to type 'MediasAction'.
      Type 'Observable<{ type: MediasActionTypes; payload: { medias: IMediasOrigin[]; }; }>' is not assignable to type 'MediasAction'.
        Type 'Observable<{ type: MediasActionTypes; payload: { medias: IMediasOrigin[]; }; }>' is missing the following properties from type 'IMediasOriginsFetchAllFailedAction': type, payload

From what I get, it seems like there is a conflict between my IMediasOriginsFetchAllSuccessAction and IMediasOriginsFetchAllFailedAction at the payload level which makes one of them not assignable to type MediasAction. But isn't that the whole purpose of the | operator when creating types?

Utopiad
  • 107
  • 2
  • 13
  • What if you add `mergeAll()` immediately after `api('...')`. The reason is that `api` itself creates an observable, which you haven't subscribed to yet, meaning that the next `map` operator in the chain would receive the observable, instead of its actual results. – Andrei Gătej Sep 24 '20 at 07:26
  • Thanks for your insight, unfortunately I made changes since then and removed the `api` operator that was one my own. All changes have been reflected in the original post. – Utopiad Sep 24 '20 at 09:09
  • I think it's the same problem. Your `map` operator returns an observable `of(...)`. Try to remove `of()` and simply return the object `{ type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL_SUCCESS, payload: { medias: response }, }` from it. – Andrei Gătej Sep 24 '20 at 09:19
  • It adds two lines to the error message: `Types of property 'type' are incompatible. Type 'MediasActionTypes' is not assignable to type 'MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL'.` – Utopiad Sep 24 '20 at 09:30
  • Could you reproduce the error in a StackBlitz or something similar? – Andrei Gătej Sep 24 '20 at 09:30
  • 1
    Yep, I reproduced it here on codesandbox: https://codesandbox.io/s/epics-and-typescript-9uigq – Utopiad Sep 24 '20 at 10:26

1 Answers1

1

You could solve this by adding as const to your MediasActionTypes.* when returning actions:

const getAllMediasOriginEpic: Epic<MediasAction, MediasAction> = (action$) => {
  const request = new ApiClient();
  return action$.pipe(
    filter(isOfType(MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL)),
    switchMap((action) =>
      from(request.getAllOriginMedias()).pipe(
        map((response: IMediasOrigin[]) => ({
          type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL_SUCCESS as const,
          payload: { medias: response }
        })),
        catchError((error: Error) =>
          of({
            type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL_FAILED as const,
            payload: { error: error.message }
          })
        )
      )
    )
  );
};

This is because when you use something like this:

map((response: IMediasOrigin[]) => ({
    type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL_SUCCESS,
    payload: { medias: response }
  })),

The Observable's type is: { type: MediasActionTypes }, .... As you've seen in your error

Type 'MediasActionTypes' is not assignable to type 'MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL_FAILED'

'MediasActionTypes' refers to a union and MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL_FAILED to a concrete type. So, by using type: MediasActionTypes.MEDIAS_ORIGINS_FETCH_ALL_SUCCESS as const you're narrowing the type.

Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31