score:4

Accepted answer

The problem is that redux-toolkit obscures the actions so it's hard to know what the action types are. Whereas in a traditional redux setup they are just a bunch of constants.

type T = ReturnType<typeof authSlice.actions.loginStart>['type']; // T is string

// have to create an action to find the actual value of the string
const action = authSlice.actions.loginStart({username: "name", password: "pw"});
const type = action.type;
console.log(type);

It appears that the action.type for the action created by authSlice.actions.loginStart is "auth/loginStart" and its type is just string rather than a specific string literal. The formula is ${sliceName}/${reducerName}. So the ofType becomes

ofType("auth/loginStart")

Now for the generic annotations. Our authEpic is taking a login start action and converting it to a login completed action. We can get those two types in a round-about way by looking at authSlice:

type LoginStartAction = ReturnType<typeof authSlice.actions.loginStart>`)

But this is silly because we already know the action types from when we created authSlice. The action type is the PayloadAction inside of your CaseReducer. Let's alias and export those:

export type LoginStartAction = PayloadAction<{ username: string; password: string }>;

export type LoginCompletedAction = PayloadAction<{ token: string }>;

These are the types that you'll use for the case reducers:

const loginStart: CaseReducer<AuthState, LoginStartAction> = ...

const loginCompleted: CaseReducer<AuthState, LoginCompletedAction> = ...

I'm not too familiar with observables and epics, but I think the typings that you want on your authEpic are:

export default (action$: ActionsObservable<LoginStartAction>) => action$.pipe(
    ofType("auth/loginStart"),
    switchMap<LoginStartAction, ObservableInput<LoginCompletedAction>>(
        action => ajax.post(
            "url", action.payload
        )
    )
    ...
);

score:0

I had a read through the redux-toolkit docs and tried to apply it to redux-observable as best I could. This is what I came up with.

import { delay, mapTo} from 'rxjs/operators';
import { ofType } from 'redux-observable';
import { createSlice} from "@reduxjs/toolkit";

const delayTime = 1000
export type pingValues = 'PING' | 'PONG'

export interface PingState {
    value: pingValues,
    isStarted: boolean,
    count: number
}

const initialState: PingState = {
    value: 'PING',
    isStarted: false,
    count: 0
};

export const pingSlice = createSlice({
    name: 'ping',
    initialState,
    reducers: {
        // createSlice does some cool things here. It creates an Action Create function (setPing()) and an Action Type, with a type property 'ping/setPing'. It adds that string as ToString() on the function as well which we can use in the ofType() calls with rxjs
        setPing: (state => {
            state.value = 'PING'
            state.isStarted = true
            state.count++;
        }),
        setPong: (state => {
            state.value = 'PONG';
            state.isStarted = true;
            state.count++;
        })
    },
});

// Epics
export const pingEpic = (action$:any) => action$.pipe(
    ofType(setPing), // Pulling out the string 'ping/setPing' from the action creator 
    delay(delayTime),// Asynchronously wait 1000ms then continue
    mapTo(setPong()) // here we're executing the action creator to create an action Type 'plain old javascript object' 
);

export const pongEpic = (action$:any) => action$.pipe(
    ofType(setPong), 
    delay(delayTime),
    mapTo(setPing())
);
 

// Export the actionCreators
export const { setPing, setPong } = pingSlice.actions;

// export the reducer
export default pingSlice.reducer;

score:12

In redux-toolkit, you should use the action.match function in a filter instead of ofType for a similar workflow, as stated in the documentation.

This example from the docs will work with all RTK actions, no matter if created with createAction, createSlice or createAsyncThunk.

import { createAction, Action } from '@reduxjs/toolkit'
import { Observable } from 'rxjs'
import { map, filter } from 'rxjs/operators'

const increment = createAction<number>('INCREMENT')

export const epic = (actions$: Observable<Action>) =>
  actions$.pipe(
    filter(increment.match),
    map((action) => {
      // action.payload can be safely used as number here (and will also be correctly inferred by TypeScript)
      // ...
    })
  )

Related Query

More Query from same tag