3

New to typescript + redux ecosystem here.

How do I properly encapsulate type information into async actions when using redux-actions, redux-thunk and redux-promise-middleware in TypeScript?

An example of authentication:

/* actions */
const login = createAction(LOGIN, authService.login);


/* authService */
async function login(payload: LoginPayload): Promise<LoginResponse> {
 // ... authenticate here.
}

Since I'm using redux-promise-middleware, the actions LOGIN_PENDING, LOGIN_FULFILLED/LOGIN_REJECTED are dispatched automatically. How do I create types for these such that the reducer can figure out what action object it's dealing with?

Since redux-actions follows FSA, _FULFILLED should have action.payload. _REJECTED should have action.error

/* reducer */
function loginReducer(state: AppState, action: AuthAction) {
  switch (action.type) {
    case LOGIN_FULFILLED:
      // action.payload should be defined as LoginResponse object here.
      // action.error shouldnt be present.
    case LOGIN_REJECTED:
      // action.error should be defined
  }
}

How would I go about creating the AuthAction type? I'm guessing it should be a union type of each of the individual action types (which can be union types on their own). redux-actions also provides Action and BaseAction types for this.

2 Answers2

0

The "normal" way to do it is to specify all "action interfaces" and a union type down to the reducer. Then switch on the type. But from the example code its not clear if the AuthAction type is a union type...

e.g.

type T = Object; //Your resolve data type
interface ILoginAction {type: "LOGIN", payload: {promise: Promise<T> }}
interface ILoginRejectedAction {type: "LOGIN_REJECTED", error: YourErrorType }
interface ILoginFulfilledAction {type: "LOGIN_FULFILLED", payload: {data: T }}

export type LoginActions = ILoginAction | ILoginRejectedAction | ILoginFulfilledAction

Reducer:

import { LoginActions } from "./actions"; //Or where your actions are
function loginReducer(state: AppState, action: LoginActions) {
  switch (action.type) {
    case "LOGIN":
      // action.payload should be defined as LoginResponse object here.
      // action.error shouldnt be present.
    case "LOGIN_REJECTED":
      // action.error should be defined
  }
}

You can probably create this union in a smarter way using some generics but this is the manual approach.

Per Svensson
  • 751
  • 6
  • 14
  • That is the question. What would be `AuthActions` such that each case block knows the interface from which `action` comes from. I've edited the question for clarity. – Srishan Supertramp Oct 18 '17 at 12:21
  • Then its the LoginActions union type in my answer above. Iv'e read through the .d.ts files for the middleware and i can see any generics from it. So this means you have to define them like i did and import the union type in your reducer. – Per Svensson Oct 18 '17 at 13:21
0

I would recommend using createAsyncAction instead of createAction. That way you can define the typings as follows:

/* actions */
const login = createAsyncAction<
  typeof LOGIN,
  LoginResponse, // no need to wrap in Promise<>
  null, // this is optional Meta,
  [LoginPayload] // action creator arguments wrapped in array
>(LOGIN, authService.login);

But i'm afraid I don't have an answer for when login is a thunk:

/* actions */
const loginAction = createAsyncAction(LOGIN, authService.login)
const loginActionCreator = (data: LoginPayload) => async(dispatch: Dispatch) => {
  await dispatch(loginAction(data));
}
// ERROR: () => void is not assignable to () => (dispatch) => void (pseudo code)
alextrastero
  • 3,703
  • 2
  • 21
  • 31