Hur man skriver Redux-åtgärder och Redux-reducers i TypeScript?


Vad är det bästa sättet att casta action-parametern i en redux-reducer med TypeScript? Det kommer att finnas flera action-interface som kan förekomma som alla utökar ett basinterface med en egenskap type. De utökade action-interfacen kan ha fler egenskaper som alla är olika mellan action-interfacen. Här är ett exempel nedan:

interface IAction {
    type: string
}

interface IActionA extends IAction {
    a: string
}

interface IActionB extends IAction {
    b: string
}

const reducer = (action: IAction) {
    switch (action.type) {
        case 'a':
            return console.info('action a: ', action.a) // property 'a' does not exists on type IAction

        case 'b':
            return console.info('action b: ', action.b) // property 'b' does not exists on type IAction         
    }
}

Problemet är att action behöver kastas som en typ som har tillgång till både IActionA och IActionB så att reducern kan använda både action.a och action.a utan att kasta ett fel.

Jag har flera idéer om hur man kan kringgå detta problem:

  1. Kasta action till any.
  2. Använd valfria gränssnittselement.

exempel :

interface IAction {
    type: string
    a?: string
    b?: string
}
  1. Använd olika reducers för varje handlingstyp.

Vad är det bästa sättet att organisera Actions/Reducers i TypeScript? Tack på förhand!

Svar #1

Med Typescript 2:s Tagged Union Types kan du göra följande

interface ActionA {
    type: 'a';
    a: string
}

interface ActionB {
    type: 'b';
    b: string
}

type Action = ActionA | ActionB;

function reducer(action:Action) {
    switch (action.type) {
        case 'a':
            return console.info('action a: ', action.a) 
        case 'b':
            return console.info('action b: ', action.b)          
    }
}

Svar #2

Jag har ett Action-gränssnitt

export interface Action<T, P> {
    readonly type: T;
    readonly payload?: P;
}

Jag har en createAction-funktion:

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

Jag har en konstant för handlingstyp:

const IncreaseBusyCountActionType = "IncreaseBusyCount";

Och jag har ett gränssnitt för åtgärden (kolla in det coola användandet av typeof):

type IncreaseBusyCountAction = Action<typeof IncreaseBusyCountActionType, void>;

Jag har en funktion för att skapa åtgärder:

function createIncreaseBusyCountAction(): IncreaseBusyCountAction {
    return createAction(IncreaseBusyCountActionType, null);
}

Nu ser min reducer ungefär ut så här:

type Actions = IncreaseBusyCountAction | DecreaseBusyCountAction;

function busyCount(state: number = 0, action: Actions) {
    switch (action.type) {
        case IncreaseBusyCountActionType: return reduceIncreaseBusyCountAction(state, action);
        case DecreaseBusyCountActionType: return reduceDecreaseBusyCountAction(state, action);
        default: return state;
    }
}

Och jag har en reducerfunktion per åtgärd:

function reduceIncreaseBusyCountAction(state: number, action: IncreaseBusyCountAction): number {
    return state + 1;
}

Svar #3

Jag kanske blir sen till dansen men enum är bäst!

enum ActionTypes {
  A = 'ANYTHING_HERE_A',
  B = 'ANYTHING_HERE_B',
}

interface IActionA {
  type: ActionTypes.A;
  a: string;
}

interface IActionB {
  type: ActionTypes.B;
  b: string;
}

type IAction = IActionA | IActionB

const reducer = (action: IAction) {
  switch (action.type) {
    case ActionTypes.A:
      return console.info('action a: ', action.a)

    case ActionTypes.B:
      return console.info('action b: ', action.b)
    }
}

Svar #4

Här är en smart lösning från Github-användaren aikoven från https://github.com/reactjs/redux/issues/992#issuecomment-191152574:

type Action<TPayload> = {
    type: string;
    payload: TPayload;
}

interface IActionCreator<P> {
  type: string;
  (payload: P): Action<P>;
}

function actionCreator<P>(type: string): IActionCreator<P> {
  return Object.assign(
    (payload: P) => ({type, payload}),
    {type}
  );
}

function isType<P>(action: Action<any>,
                          actionCreator: IActionCreator<P>): action is Action<P> {
  return action.type === actionCreator.type;
}

Använd actionCreator<P> för att definiera dina actions och action creators:

export const helloWorldAction = actionCreator<{foo: string}>('HELLO_WORLD');
export const otherAction = actionCreator<{a: number, b: string}>('OTHER_ACTION');

Använd den användardefinierade typvakt isType<P> i reduceraren:

function helloReducer(state: string[] = ['hello'], action: Action<any>): string[] {
    if (isType(action, helloWorldAction)) { // type guard
       return [...state, action.payload.foo], // action.payload is now {foo: string}
    } 
    else if(isType(action, otherAction)) {
        ...

Och att skicka en åtgärd:

dispatch(helloWorldAction({foo: 'world'})
dispatch(otherAction({a: 42, b: 'moon'}))

Jag rekommenderar att läsa igenom hela kommentarskedjan för att hitta andra alternativ eftersom flera lika bra lösningar presenteras där.

Svar #5

Så här gör jag det:

IAction.ts

import {Action} from 'redux';

/**
 * https://github.com/acdlite/flux-standard-action
 */
export default interface IAction<T> extends Action<string> {
    type: string;
    payload?: T;
    error?: boolean;
    meta?: any;
}

UserAction.ts

import IAction from '../IAction';
import UserModel from './models/UserModel';

export type UserActionUnion = void | UserModel;

export default class UserAction {

    public static readonly LOAD_USER: string = 'UserAction.LOAD_USER';
    public static readonly LOAD_USER_SUCCESS: string = 'UserAction.LOAD_USER_SUCCESS';

    public static loadUser(): IAction<void> {
        return {
            type: UserAction.LOAD_USER,
        };
    }

    public static loadUserSuccess(model: UserModel): IAction<UserModel> {
        return {
            payload: model,
            type: UserAction.LOAD_USER_SUCCESS,
        };
    }

}

UserReducer.ts

import UserAction, {UserActionUnion} from './UserAction';
import IUserReducerState from './IUserReducerState';
import IAction from '../IAction';
import UserModel from './models/UserModel';

export default class UserReducer {

    private static readonly _initialState: IUserReducerState = {
        currentUser: null,
        isLoadingUser: false,
    };

    public static reducer(state: IUserReducerState = UserReducer._initialState, action: IAction<UserActionUnion>): IUserReducerState {
        switch (action.type) {
            case UserAction.LOAD_USER:
                return {
                    ...state,
                    isLoadingUser: true,
                };
            case UserAction.LOAD_USER_SUCCESS:
                return {
                    ...state,
                    isLoadingUser: false,
                    currentUser: action.payload as UserModel,
                };
            default:
                return state;
        }
    }

}

IUserReducerState.ts

import UserModel from './models/UserModel';

export default interface IUserReducerState {
    readonly currentUser: UserModel;
    readonly isLoadingUser: boolean;
}

UserSaga.ts

import IAction from '../IAction';
import UserService from './UserService';
import UserAction from './UserAction';
import {put} from 'redux-saga/effects';
import UserModel from './models/UserModel';

export default class UserSaga {

    public static* loadUser(action: IAction<void> = null) {
        const userModel: UserModel = yield UserService.loadUser();

        yield put(UserAction.loadUserSuccess(userModel));
    }

}

UserService.ts

import HttpUtility from '../../utilities/HttpUtility';
import {AxiosResponse} from 'axios';
import UserModel from './models/UserModel';
import RandomUserResponseModel from './models/RandomUserResponseModel';
import environment from 'environment';

export default class UserService {

    private static _http: HttpUtility = new HttpUtility();

    public static async loadUser(): Promise<UserModel> {
        const endpoint: string = `${environment.endpointUrl.randomuser}?inc=picture,name,email,phone,id,dob`;
        const response: AxiosResponse = await UserService._http.get(endpoint);
        const randomUser = new RandomUserResponseModel(response.data);

        return randomUser.results[0];
    }

}

https://github.com/codeBelt/typescript-hapi-react-hot-loader-example

Relaterade artiklar