Подтвердить что ты не робот

Как ввести действия Redux и редукторы Redux в TypeScript?

Каков наилучший способ приведения параметра action в редукторном редукторе с машинописью? Будет несколько интерфейсов действий, которые могут расширять базовый интерфейс с типом свойства. Расширенные интерфейсы действий могут иметь больше свойств, которые различаются между интерфейсами действий. Вот пример ниже:

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         
    }
}

Проблема заключается в том, что action должно быть приведено к типу, который имеет доступ к IActionA и IActionB чтобы редуктор мог использовать как action.a и action.a не action.a ошибку.

У меня есть несколько идей, как обойти эту проблему:

  1. Cast action на any.
  2. Используйте дополнительные элементы интерфейса.

пример:

interface IAction {
    type: string
    a?: string
    b?: string
}
  1. Используйте разные редукторы для каждого типа действия.

Каков наилучший способ организовать Action/Reducers в машинописи? Заранее спасибо!

4b9b3361

Ответ 1

С Typescript 2 Tagged Union Types вы можете сделать следующее

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)          
    }
}

Ответ 2

У меня есть интерфейс Action

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

У меня есть функция createAction:

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

У меня есть константа типа действия:

const IncreaseBusyCountActionType = "IncreaseBusyCount";

И у меня есть интерфейс для действия (проверьте прохладное использование typeof):

type IncreaseBusyCountAction = Action<typeof IncreaseBusyCountActionType, void>;

У меня есть функция создания действия:

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

Теперь мой редуктор выглядит примерно так:

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;
    }
}

И у меня есть функция редуктора за действие:

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

Ответ 3

Вот умное решение от пользователя Github aikoven из 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;
}

Используйте actionCreator<P> для определения ваших действий и создателей действий:

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

Используйте определенный пользователем тип guard isType<P> в редукторе:

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)) {
        ...

И отправить действие:

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

Я рекомендую прочитать всю цепочку комментариев, чтобы найти другие варианты, так как там есть несколько одинаково хороших решений.

Ответ 4

Для относительно простого редуктора вы, вероятно, можете просто использовать защитные устройства типа:

function isA(action: IAction): action is IActionA {
  return action.type === 'a';
}

function isB(action: IAction): action is IActionB {
  return action.type === 'b';
}

function reducer(action: IAction) {
  if (isA(action)) {
    console.info('action a: ', action.a);
  } else if (isB(action)) {
    console.info('action b: ', action.b);
  }
}

Ответ 5

Вот как я это делаю:

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

Ответ 6

Две части проблемы

В нескольких вышеупомянутых комментариях упоминается концепция/функция 'actionCreator´ - взгляните на пакет redux-actions (и соответствующие определения TypeScript), который решает первую часть проблемы: создание функций создателя действий, которые имеют информацию о типе TypeScript, определяющую тип полезной нагрузки действия.

Вторая часть проблемы заключается в объединении функций редуктора в единый редуктор без шаблонного кода и безопасным для типов способом (как был задан вопрос о TypeScript).

Решение

Объедините пакеты redux-actions и redux-actions-ts-reducer:

1) Создайте функции actionCreator, которые можно использовать для создания действия с желаемым типом и полезной нагрузкой при отправке действия:

import { createAction } from 'redux-actions';

const negate = createAction('NEGATE'); // action without payload
const add = createAction<number>('ADD'); // action with payload type 'number'

2) Создать редуктор с начальным состоянием и функциями редуктора для всех связанных действий:

import { ReducerFactory } from 'redux-actions-ts-reducer';

// type of the state - not strictly needed, you could inline it as object for initial state
class SampleState {
    count = 0;
}

// creating reducer that combines several reducer functions
const reducer = new ReducerFactory(new SampleState())
    // 'state' argument and return type is inferred based on 'new ReducerFactory(initialState)'.
    // Type of 'action.payload' is inferred based on first argument (action creator)
    .addReducer(add, (state, action) => {
        return {
            ...state,
            count: state.count + action.payload,
        };
    })
    // no point to add 'action' argument to reducer in this case, as 'action.payload' type would be 'void' (and effectively useless)
    .addReducer(negate, (state) => {
        return {
            ...state,
            count: state.count * -1,
        };
    })
    // chain as many reducer functions as you like with arbitrary payload types
    ...
    // Finally call this method, to create a reducer:
    .toReducer();

Как видно из комментариев, вам не нужно писать какие-либо аннотации типов TypeScript, но все типы выводятся (так что это работает даже с noImplicitAny компилятора noImplicitAny TypeScript)

Если вы используете действия из некоторого фреймворка, который не предоставляет создателей действий с redux-action (и вы не хотите создавать их сами), или у вас есть унаследованный код, который использует строковые константы для типов действий, вы также можете добавить для них редукторы:

const SOME_LIB_NO_ARGS_ACTION_TYPE = '@@some-lib/NO_ARGS_ACTION_TYPE';
const SOME_LIB_STRING_ACTION_TYPE = '@@some-lib/STRING_ACTION_TYPE';

const reducer = new ReducerFactory(new SampleState())
    ...
    // when adding reducer for action using string actionType
    // You should tell what is the action payload type using generic argument (if You plan to use 'action.payload')
    .addReducer<string>(SOME_LIB_STRING_ACTION_TYPE, (state, action) => {
        return {
            ...state,
            message: action.payload,
        };
    })
    // action.payload type is 'void' by default when adding reducer function using 'addReducer(actionType: string, reducerFunction)'
    .addReducer(SOME_LIB_NO_ARGS_ACTION_TYPE, (state) => {
        return new SampleState();
    })
    ...
    .toReducer();

так что легко начать без рефакторинга Вашей кодовой базы.

Диспетчерские действия

Вы можете отправлять действия даже без redux например так:

const newState = reducer(previousState, add(5));

но диспетчеризировать действие с помощью redux проще - используйте функцию dispatch(...) как обычно:

dispatch(add(5));
dispatch(negate());
dispatch({ // dispatching action without actionCreator
    type: SOME_LIB_STRING_ACTION_TYPE,
    payload: newMessage,
});

Признание: я являюсь автором redux-actions-ts-reducer, который я сегодня открыл.

Ответ 7

вы можете сделать следующее:

если вы ожидаете только одного из IActionA или IActionB, вы можете ограничить тип по крайней мере и определить свою функцию как

const reducer = (action: (IActionA | IActionB)) => {
   ...
}

Теперь, дело в том, что вам все равно нужно выяснить, какой тип он есть. Вы можете полностью добавить свойство type, но тогда вы должны его установить где-нибудь, а интерфейсы - только наложения на структуры объектов. Вы можете создать классы действий и установить ctor для этого типа.

В противном случае вам нужно проверить объект на что-то еще. В вашем случае вы можете использовать hasOwnProperty и в зависимости от этого, применить его к правильному типу:

const reducer = (action: (IActionA | IActionB)) => {
    if(action.hasOwnProperty("a")){
        return (<IActionA>action).a;
    }

    return (<IActionB>action).b;
}

Это все равно будет работать при компиляции в JavaScript.

Ответ 8

Решение @Jussi_K, на которое ссылается, хорошо, потому что оно является общим.

Однако, я нашел способ, который мне нравится лучше, по пяти пунктам:

  • Он имеет свойства действия непосредственно на объекте действия, а не в объекте "полезной нагрузки", который короче. (хотя, если вы предпочитаете "полезную нагрузку", просто раскомментируйте дополнительную строку в конструкторе)
  • Он может быть проверен типом в редукторах с помощью простого action.Is(Type), а не clunkier isType(action, createType).
  • Логика, содержащаяся в одном классе, вместо разметки amonst type Action<TPayload>, interface IActionCreator<P>, function actionCreator<P>(), function isType<P>().
  • Он использует простые, реальные классы вместо "создателей действий" и интерфейсов, которые, на мой взгляд, более читабельны и расширяемы. Чтобы создать новый тип действия, просто class MyAction extends Action<{myProp}> {}.
  • Он обеспечивает согласованность между свойством class-name и type, просто вычисляя type как имя класса/конструктора. Это соответствует принципу DRY, в отличие от другого решения, которое имеет как функцию helloWorldAction, так и магическую строку HELLO_WORLD.

Во всяком случае, для реализации этой альтернативной настройки:

Сначала скопируйте этот общий класс Action:

class Action<Payload> {
    constructor(payload: Payload) {
        this.type = this.constructor.name;
        //this.payload = payload;
        Object.assign(this, payload);
    }
    type: string;
    payload: Payload; // stub; needed for Is() method type-inference to work, for some reason

    Is<Payload2>(actionType: new(..._)=>Action<Payload2>): this is Payload2 {
        return this.type == actionType.name;
        //return this instanceof actionType; // alternative
    }
}

Затем создайте производные классы Action:

class IncreaseNumberAction extends Action<{amount: number}> {}
class DecreaseNumberAction extends Action<{amount: number}> {}

Затем для использования в редукторной функции:

function reducer(state, action: Action<any>) {
    if (action.Is(IncreaseNumberAction))
        return {...state, number: state.number + action.amount};
    if (action.Is(DecreaseNumberAction))
        return {...state, number: state.number - action.amount};
    return state;
}

Если вы хотите создать и отправить действие, просто выполните:

dispatch(new IncreaseNumberAction({amount: 10}));

Как и при решении @Jussi_K, каждый из этих шагов безопасен по типу.

ИЗМЕНИТЬ

Если вы хотите, чтобы система была совместима с анонимными объектами действия (например, из устаревшего кода или десериализованного состояния), вы можете вместо этого использовать эту статическую функцию в своих редукторах:

function IsType<Payload>(action, actionType: new(..._)=>Action<Props>): action is Payload {
    return action.type == actionType.name;
}

И используйте его так:

function reducer(state, action: Action<any>) {
    if (IsType(action, IncreaseNumberAction))
        return {...state, number: state.number + action.amount};
    if (IsType(action, DecreaseNumberAction))
        return {...state, number: state.number - action.amount};
    return state;
}

Другой вариант - добавить метод Action.Is() на глобальный Object.prototype с помощью Object.defineProperty. Это то, что я сейчас делаю, хотя большинству людей это не нравится, поскольку он загрязняет прототип.

EDIT 2

Несмотря на то, что он все равно будет работать, Redux жалуется, что "Действия должны быть обычными объектами. Используйте специальное промежуточное ПО для асинхронных действий".

Чтобы исправить это, вы можете:

  • Удалите проверки isPlainObject() в Редуксе.
  • Выполните одну из модификаций моего редактирования выше, добавьте эту строку в конец конструктора класса Action: (он удаляет связь между экземпляром и классом)
Object.setPrototypeOf(this, Object.getPrototypeOf({}));

Ответ 9

Чтобы получить неявные типы безопасности без необходимости писать интерфейсы для каждого действия, вы можете использовать этот подход (вдохновленный функцией returntypeof здесь: https://github.com/piotrwitek/react-redux-typescript#returntypeof-polyfill)

import { values } from 'underscore'

/**
 * action creator (declaring the return type is optional, 
 * but you can make the props readonly)
 */
export const createAction = <T extends string, P extends {}>(type: T, payload: P) => {
  return {
    type,
    payload
  } as {
    readonly type: T,
    readonly payload: P
  }
}

/**
 * Action types
 */
const ACTION_A = "ACTION_A"
const ACTION_B = "ACTION_B"

/**
 * actions
 */
const actions = {
  actionA: (count: number) => createAction(ACTION_A, { count }),
  actionB: (name: string) => createAction(ACTION_B, { name })
}

/**
 * create action type which you can use with a typeguard in the reducer
 * the actionlist variable is only needed for generation of TAction
 */
const actionList = values(actions).map(returnTypeOf)
type TAction = typeof actionList[number]

/**
 * Reducer
 */
export const reducer = (state: any, action: TAction) => {
  if ( action.type === ACTION_A ) {
    console.log(action.payload.count)
  }
  if ( action.type === ACTION_B ) {
    console.log(action.payload.name)
    console.log(action.payload.count) // compile error, because count does not exist on ACTION_B
  }
  console.log(action.payload.name) // compile error because name does not exist on every action
}

Ответ 10

С Typescript v2 вы можете сделать это довольно легко, используя типы объединения со средствами защиты типов и собственными типами действий и редукторов Redux без необходимости использования дополнительных сторонних библиотек и без применения общей формы для всех действий (например, с помощью payload),

Таким образом, ваши действия будут правильно напечатаны в ваших предложениях catch редуктора, как и возвращаемое состояние.

import {
  Action,
  Reducer,
} from 'redux';

interface IState {
  tinker: string
  toy: string
}

type IAction = ISetTinker
  | ISetToy;

const SET_TINKER = 'SET_TINKER';
const SET_TOY = 'SET_TOY';

interface ISetTinker extends Action<typeof SET_TINKER> {
  tinkerValue: string
}
const setTinker = (tinkerValue: string): ISetTinker => ({
  type: SET_TINKER, tinkerValue,
});
interface ISetToy extends Action<typeof SET_TOY> {
  toyValue: string
}
const setToy = (toyValue: string): ISetToy => ({
  type: SET_TOY, toyValue,
});

const reducer: Reducer<IState, IAction> = (
  state = { tinker: 'abc', toy: 'xyz' },
  action
) => {
  // action is IAction
  if (action.type === SET_TINKER) {
    // action is ISetTinker
    // return { ...state, tinker: action.wrong } // doesn't typecheck
    // return { ...state, tinker: false } // doesn't typecheck
    return {
      ...state,
      tinker: action.tinkerValue,
    };
  } else if (action.type === SET_TOY) {
    return {
      ...state,
      toy: action.toyValue
    };
  }

  return state;
}

В основном это то, что предлагает @Sven Efftinge, дополнительно проверяя тип возвращаемого значения редуктора.

Ответ 11

Я являюсь автором ts-redux-actions-reducer-factory и представляю вам это как еще одно решение поверх других. Этот пакет выводит действие создателем действия или определенным вручную типом действия и - этим новым - состоянием. Таким образом, каждый редуктор принимает во внимание тип возврата предыдущих редукторов и, следовательно, представляет возможное расширенное состояние, которое должно быть инициализировано в конце, если не сделано в начале. Это своего рода особенное в использовании, но может упростить ввод текста.

Но вот полное возможное решение на основе вашей проблемы:

import { createAction } from "redux-actions";
import { StateType } from "typesafe-actions";
import { ReducerFactory } from "../../src";

// Type constants
const aType = "a";
const bType = "b";

// Container a
interface IActionA {
    a: string;
}

// Container b
interface IActionB {
    b: string;
}

// You define the action creators:
// - you want to be able to reduce "a"
const createAAction = createAction<IActionA, string>(aType, (a) => ({ a }));
// - you also want to be able to reduce "b"
const createBAction = createAction<IActionB, string>(aType, (b) => ({ b }));

/*
 * Now comes a neat reducer factory into the game and we
 * keep a reference to the factory for example purposes
 */
const factory = ReducerFactory
    .create()
    /*
     * We need to take care about other following reducers, so we normally want to include the state
     * by adding "...state", otherwise only property "a" would survive after reducing "a".
     */
    .addReducer(createAAction, (state, action) => ({
        ...state,
        ...action.payload!,
    }))
    /*
     * By implementation you are forced to initialize "a", because we
     * now know about the property "a" by previous defined reducer.
     */
    .addReducer(createBAction, (state, action) => ({
        ...state,
        ...action.payload!,
    }))
    /**
     * Now we have to call 'acceptUnknownState' and are forced to initialize the reducer state.
     */
    .acceptUnknownState({
        a: "I am A by default!",
        b: "I am B by default!",
    });

// At the very end, we want the reducer.
const reducer = factory.toReducer();

const initialState = factory.initialKnownState;
// { a: "I am A by default!", b: "I am B by default!" }

const resultFromA = reducer(initialState, createAAction("I am A!"));
// { a: "I am A!", b: "I am B by default!" }

const resultFromB = reducer(resultFromA, createBAction("I am B!"));
// { a: "I am A!", b: "I am B!" }

// And when you need the new derived type, you can get it with a module like @typesafe-actions
type DerivedType = StateType<typeof reducer>;

// Everything is type-safe. :)
const derivedState: DerivedType = initialState;

Ответ 12

Существуют библиотеки, которые объединяют большую часть кода, упомянутого в других ответах: aikoven/typescript-fsa и dphilipson/typescript-fsa-redurs.

С этими библиотеками все ваши действия и код редуктора статически типизируются и читаются:

import actionCreatorFactory from "typescript-fsa";
const actionCreator = actionCreatorFactory();

interface State {
  name: string;
  balance: number;
  isFrozen: boolean;
}

const INITIAL_STATE: State = {
  name: "Untitled",
  balance: 0,
  isFrozen: false,
};

const setName = actionCreator<string>("SET_NAME");
const addBalance = actionCreator<number>("ADD_BALANCE");
const setIsFrozen = actionCreator<boolean>("SET_IS_FROZEN");

...

import { reducerWithInitialState } from "typescript-fsa-reducers";

const reducer = reducerWithInitialState(INITIAL_STATE)
  .case(setName, (state, name) => ({ ...state, name }))
  .case(addBalance, (state, amount) => ({
    ...state,
    balance: state.balance + amount,
  }))
  .case(setIsFrozen, (state, isFrozen) => ({ ...state, isFrozen }));

Ответ 13

вы можете определить свое действие примерно так:

// src/actions/index.tsx
import * as constants from '../constants'

export interface IncrementEnthusiasm {
    type: constants.INCREMENT_ENTHUSIASM;
}

export interface DecrementEnthusiasm {
    type: constants.DECREMENT_ENTHUSIASM;
}

export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;

export function incrementEnthusiasm(): IncrementEnthusiasm {
    return {
        type: constants.INCREMENT_ENTHUSIASM
    }
}

export function decrementEnthusiasm(): DecrementEnthusiasm {
    return {
        type: constants.DECREMENT_ENTHUSIASM
    }
}

и так, вы можете определить свой редуктор следующим образом:

//src/reducers/index.tsx

import { EnthusiasmAction } from '../actions';
import { StoreState } from '../types/index';
import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index';

export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState {
  switch (action.type) {
    case INCREMENT_ENTHUSIASM:
      return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
    case DECREMENT_ENTHUSIASM:
      return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1) };
  }
  return state;
}

Завершить официальные документы: https://github.com/Microsoft/TypeScript-React-Starter#adding-a-reducer

Ответ 14

Если вам нужно исправить вашу реализацию точно так же, как вы разместили, вот как ее исправить и заставить ее работать, используя утверждения типа, соответственно, как показано ниже:

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: ', (<IActionA>action).a) // property 'a' exists because you're using type assertion <IActionA>

      case 'b':
          return console.info('action b: ', (<IActionB>action).b) // property 'b' exists because you're using type assertion <IActionB>
  }
}

Вы можете узнать больше в разделе "Типы гвардейцев и типы дифференцирования" официальной документации: https://www.typescriptlang.org/docs/handbook/advanced-types.html

Ответ 15

Чтобы быть справедливым, существует множество способов ввода действий, но я нахожу этот очень прямым и имеет менее возможный шаблон (а) уже обсуждался в этом разделе).

Этот подход пытается ввести ключ, называемый "полезной нагрузкой" действий.

Проверьте этот пример

Ответ 16

В последнее время я использовал этот подход:

export abstract class PlainAction {
    public abstract readonly type: any;
    constructor() {
        return Object.assign({}, this);
    }
}

export abstract class ActionWithPayload<P extends object = any> extends PlainAction {
    constructor(public readonly payload: P) {
        super();
    }
}

export class BeginBusyAction extends PlainAction {
    public readonly type = "BeginBusy";
}

export interface SendChannelMessageActionPayload {
    message: string;
}

export class SendChannelMessageAction
    extends ActionWithPayload<SendChannelMessageActionPayload>
{
    public readonly type = "SendChannelMessage";
    constructor(
        message: string,
    ) {
        super({
            message,
        });
    }
}

Здесь:

constructor() {
    return Object.assign({}, this);
}

гарантирует, что Action - все простые объекты. Теперь вы можете делать такие действия: const action = new BeginBusyAction(). (yay\o/)

Ответ 17

Вот как вы можете сделать это с помощью redux-fluent:

enter image description here enter image description here

Ответ 18

Я предлагаю использовать AnyAction потому что, согласно Redux FAQ, каждый редуктор AnyAction при каждом действии. Вот почему мы в конечном итоге просто возвращаем состояние ввода, если действие не относится к одному из типов. Иначе у нас никогда не было бы случая по умолчанию в наших коммутаторах в наших редукторах.

Смотрите: https://redux.js.org/faq/performance#won-t-calling-all-my-reducers-for-each-action-be-slow

Поэтому хорошо бы просто сделать:

import { AnyAction } from 'redux';

function myReducer(state, action: AnyAction) {
  // ...
}

Ответ 19

Вот такой подход, который я предпринял для этой проблемы:

const reducer = (action: IAction) {

    const actionA: IActionA = action as IActionA;
    const actionB: IActionB = action as IActionB;

    switch (action.type) {
        case 'a':
            // Only ever use actionA in this context
            return console.info('action a: ', actionA.a)

        case 'b':
            // Only ever use actionB in this context
            return console.info('action b: ', actionB.b)
    }
}

Я буду первым, кто признал бы здесь некоторое уродство и взлома этого подхода, но на самом деле я на самом деле нашел его на практике. В частности, я нахожу, что он упрощает чтение и обслуживание кода, поскольку цель действия заключается в имени, а также облегчает поиск.