1

I found the awesome solution about type-safety redux using typescript's ReturnType feature which is new of version 2.8.

actions/types.ts

type FunctionType = (...args: any[]) => any;
type ActionCreatorsMapObject = { [actionCreator: string]: FunctionType };

export type ActionUnion<A extends ActionCreatorsMapObject> = ReturnType<A[keyof A]>;

models/user.ts

export interface User {
    id: number;
    username: string;
    name: string;
}

actions/index.ts

import { User } from '../models/user';

interface Action<T extends string> {
    type: T;
}

interface ActionWithPayload<T extends string, P> extends Action<T> {
    payload: P;
}

export function createAction<T extends string>(type: T): Action<T>;
export function createAction<T extends string, P>(type: T, payload: P): ActionWithPayload<T, P>;
export function createAction<T extends string, P>(type: T, payload?: P) {
    return payload === undefined ? { type } : { type, payload };
}

export enum ActionTypes {
    SIMPLE_ACTION = 'SIMPLE_ACTION',
    ASYNC_ACTION = 'ASYNC_ACTION
}

export const Actions = {
    simpleAction: (value: string) => createAction(ActionTypes.SIMPLE_ACTION, value)
    asyncAction: () => {
        const token = localStorage.getItem('token');
        const request = axios.get('/', {
            headers: {
                'Authorization': token
            }
        });
        return createAction(ActionTypes.ASYNC_ACTION, request as AxiosPromise<User>);
    },
    anotherAction: (value: number) => blahblah...
};

export type Actions = ActionUnion<typeof Actions>;

Before goes into reducers, I'm using redux-promise packages which is a middleware of redux to process async dispatch. Simply, if payload is promise, then redux-promise will resolve that promise and change the payload value into result of promise.

Here is the problem. I want to use ActionUnion type when I write reducer code. But typescript doesn't know that promise is already resolved by middleware. Non-async-action works well because middleware doesn't change the payload.

user_reducer.ts

import { User } from '../models/user';
import * as fromActions from '../actions';

interface UserState {
    loginUser: User | null;
    someValue: string;
}

const initialState: UserState = {
    loginUser: null,
    someValue: ''
};

export default (state: UserState = initialState, action: fromActions.Actions) => {
    switch (action.type) {
        case fromActions.ActionTypes.SIMPLE_ACTION: {
            // typescript knows that action.payload is string. It works well.
            const data = action.payload;
            ...
        }
        case fromActions.ActionTypes.ASYNC_ACTION: {
            // typescript knows that action.payload is still promise.
            // Therefore typescript said, action.payload.data is wrong.
            const username = action.payload.data.username;
            ...
        }
        ...
    }
};

This is not a bug. This is obvious because I define action: fromActions.Action, so that typescript think "Oh, type of action parameter is ReturnValue of asyncAction function and it has promise object as payload value.".

There are two solutions that I think.

1. old style

Before 2.8 we usually define every ActionCreator's interface and union them.

export type Actions = ActionCreator1 | ActionCreator2 | ActionCreator3 | ...;

It can solve the middleware problem, but if I change action creator function's return value, then I must change matched interface manually. (That's why the new ReturnValue feature is awesome.)

2. Redefine Action type

Instead of using,

export type Actions = ActionUnion<typeof Actions>;

Let's define Action type which has promise as payload value. Let's say ActionWithPromisePayload. Below is pseudo code. I'm not good at typescript. (T_T)

// Let's define two new types.
type ActionWithPromisePayload<...>;
type ActionWithPromiseResolvedPayload<...>;

ActionWithPromisePayload is for checking whether the action object's payload has promise or not. ActionWithPromiseResolvedPayload for redefine type of action because promise is resolved by middleware.

Then, redefine type Actions by using conditional type which is also new in 2.8.

If Action has promise object as payload, then it's real type is not the ReturnValue of action but the resolved one. Below is pseudo code.

export type Actions = 
    ActionUnion<typeof Actions> extends ActionWithPromisePayload<..> ? ActionWithPromiseResolvedPayload<..> : ActionUnion<typeof Actions>;

Question is little bit messy, but the key question is, how can I define type of reducer's action parameter nicely and working well with middleware?

If there are better way, then don't care about my solutions. Thanks.

Ref. https://medium.com/@martin_hotell/improved-redux-type-safety-with-typescript-2-8-2c11a8062575

jeyongOh
  • 89
  • 2
  • 7

0 Answers0