An opinionated approach to type actions and their payload in Redux with statically type checking in Typescript. This approach removes the most possible boilerplate of your actions & their creators in React and any other frameworks.
Let's get started by installing it from npm repository
$ npm install --save-dev redux-typed-actions
or from yarn repository
$ yarn add --dev redux-typed-actions
Let's do a quick example to see how this approach can improve type checking of redux actions
-
You have an ItemX interface:
feature-x.types.tsexport interface ItemX { title: string; }
-
First we define our actions:
feature-x.actions.tsimport { defineAction, defineScenarioAction, defineSymbolAction } from 'redux-typed-actions'; import { ItemX } from './feature-x.types'; // For this action we will have number as our payload export const FeatureXAddTicketAction = defineAction<number>('[Feature X] Add Ticket'); /** * This action is special, it's called a scenario-like action * It notifies the system with status of a process covering from start to end. * You can get Start/Success/Failure/Cancel from this action generator/creator * There are 4 types that belong respectively to Start/Success/Failure/Cancel * Note: The default type for payload of success and failure is * string so you can skip them like `defineScenarioAction('MyActionName')` */ export const FeatureXLoadAction = defineScenarioAction<never, ItemX[], string>('[Feature X] Load'); // Let's have a symbol action just for fun export const FeatureXDummySymbolAction = defineSymbolAction<ItemX[]>('[Feature X] Dummy Started');
Note: You can setup your own suffix for Start/Success/Failure/Cancel of scenario actions as following example:
import { factory } from 'redux-typed-actions'; // You must set them before defining actions factory.setSuffixes({ start: '_REQUEST', cancel: '_CANCEL', success: '_SUCCESS', failure: '_FAILURE', });
-
Now we dispatch our actions:
feature-x.component.tsimport { ItemX } from './feature-x.types'; import { FeatureXAddTicketAction, FeatureXLoadAction } from '../feature-x.actions'; ... // React Redux solution to replace action creators: // Let's define our component's state interface FeatureXProps { ... addTicket: typeof FeatureXAddTicketAction.strictGet; // StrictGet makes the payload mandatory } ... class FeatureX extends React.Component<FeatureXProps> { ... addTicket = () => this.props.addTicket(1) // <- Static type checking render() { return (<button onClick={this.addTicket}>Add one ticket</button>); // /> } } // Let's hook the action to redux, and we're done export default connect(undefined, { addTicket: FeatureXAddTicketAction.strictGet })(FeatureX); // All Typescript frameworks: // Dispatching a simple action store.dispatch(FeatureXAddTicketAction.get(100)); // Let's start our scenario by dispatching our action store.dispatch(FeatureXLoadAction.get()); // Now we dispatch the success action const payload: ItemX[] = [{ title: 'item 1' }, { title: 'item 2' }]; dispatch(FeatureXLoadAction.success.get(payload)); // or simply a failure dispatch(FeatureXLoadAction.failure.get('It failed because...'));
-
Now let's take a look at our reducers to see how we can type check the payloads:
feature-x.reducers.tsimport { PlainAction } from 'redux-typed-actions'; import { ItemX } from './feature-x.types'; import { FeatureXLoadAction } from '../feature-x.actions'; export interface ItemXState { items: ItemX[]; loading: boolean; } export function reducer(state: ItemXState = InitialState, action: PlainAction): ItemXState { if (FeatureXLoadAction.is(action)) { // Within this branch our action variable has the right typings return { ...state, loading: true, }; } else if (FeatureXLoadAction.success.is(action)) { return { ...state, loading: false, items: action.payload, // <- Here we are checking types strongly }; } else { return state; } }
-
If you're fan of
redux-observables
then this part is for you:
feature-x.epics.ts(action$, store) => action$ .ofType(FeatureXLoadAction.type) .map(action => FeatureXLoadAction.cast(action)) // <- from here we will have all the typings right :) .switchMap(action => Service() .map(value => FeatureXLoadAction.success.get(repos)) .catch(() => Observable.of(FeatureXLoadAction.failure.get('Oops something went wrong!'))));
-
That's it