typed-redux-actions
v2.0.1
Published
Typed internationalization (intl/i18n) library for TypeScript/JavaScript apps.
Downloads
27
Maintainers
Readme
typed-redux-actions
This is a tiny and dependency free (except Redux) TypeScript library that helps you to concisely specify actions and process them in a typesafe way. This is what you get:
- Concisely specify strongly typed Redux conform actions saving boilerplate code.
- Type safely process these actions in the reducer.
- Let the compiler do a comprehensive check on your reducer to ensure that it handles all actions.
Motivation
Redux is great and Typescript is great. When used together in conjunction with discriminated unions the compiler will provide us the above mentioned benefits. Unfortunately to get these benefits a lot of boilerplate is necessary.
Here is what you have to do when not using this library.
// (1)
enum ActionType {
setCounter = 'COUNTER_SET',
incrementCounter = 'COUNTER_INCREMENT'
}
// (2)
interface SetAction extends Redux.Action {
readonly type: typeof ActionType.setCounter;
readonly value: number;
}
interface IncrementAction extends Redux.Action {
readonly type: typeof ActionType.incrementCounter;
readonly increment: number;
}
// (3)
type NumberAction = SetAction | IncrementAction;
// (4)
function setCounter(value: number): SetAction {
return {
type: ActionType.setCounter,
value: value
}
}
function incrementCounter(increment: number): IncrementAction {
return {
type: ActionType.incrementCounter,
increment: increment
}
}
// (5)
function reduceNumberAction(state: number, action: NumberAction): number {
switch(action.type) {
case ActionType.setCounter: return action.value;
case ActionType.incrementCounter: return state + action.increment;
}
}
// (6)
function isNumberAction(action: Redux.AnyAction): action is NumberAction {
return action.type === ActionType.setCounter || action.type === ActionType.incrementCounter;
}
function reduce(state: number, action: Redux.AnyAction): number {
if (isNumberAction(action)) {
return reduceNumberAction(state, action);
} else {
return state;
}
}
Lets dig into this and its boilerplate:
- The
type
property of an action specifies its type. There are several possibilities how to specify this type, but as we want to work with discriminated unions we need to generate type literals. We can do this definingconst
s or using anenum
like we do it here. - Now we define how our available actions look like, so that we can build up a union type in the next step. Notice how we set the types of the
type
properties to the literal types of the adequateActionType
. - Here we build our union type. As all of the union's types have the
type
property which isn't simply a string, but a type literal, we've set the stage for discriminated unions. - Now we need to define the action creator functions. Notice the boilerplate here:
- we need to specify the whole action structure again
- we need to specify the
type
-property again though we already did it in the interface declaration
- Now we can implement our reducer function which only handles our new
NumberAction
type. As we have a discriminated union scenario here, we get the desired benefits:- inside the case blocks we can safely access the properties specific to the affected action type without casting.
- we do not need to specify a
default
block (ensure to disable the related tslint rule), because the compiler is able to recognize based on the union type whether we handled all possible cases or not.
- Our job isn't done here, because a Redux reducer must return the original state if it receives an unhandled action. Thus we are defining a function which checks whether an action is of our
NumberAction
type. We then use this function in our final reducer implementation to decide whether to delegate to our specialized reducer or to return the sate.
So when adding a new action we have to do the following:
- Add a new value to the
ActionType
enum
- Define the action's type
- Define a creator function which repeats most of the type declaration
- Add the new type to the union of
NumberAction
(easy to forget) - Add the check of the
type
property to theisNumberAction
function (easy to forget) - Add the handling for this action to the reducer (the compiler will remember us to do so)
Installation
Add typed-redux-actions to your project:
npm install --save typed-redux-actions
How to use typed-redux-actions
This library provides us some tools to reduce the boilerplate shown above. Lets implment the same solution again:
// (1)
enum ActionType {
setCounter = 'COUNTER_SET',
incrementCounter = 'COUNTER_INCREMENT'
}
// (2)
function setCounter(value: number) {
return {
type: ActionType.setCounter as typeof ActionType.setCounter,
value: value
}
}
function incrementCounter(increment: number) {
return {
type: ActionType.incrementCounter as typeof ActionType.incrementCounter,
increment: increment
}
}
// (3)
const filter = actionFilter(ActionType, [
declareAction(setCounter),
declareAction(incrementCounter)
]);
// (4)
function reduceNumberAction(state: number, action: typeof filter.action): number {
switch(action.type) {
case ActionType.setCounter: return action.value;
case ActionType.incrementCounter: return state + action.increment;
}
}
// (5)
const reducer = new ActionReducer(filter, 0, reduceNumberAction);
export reducer.reduce
Lets see what's going on here and what we save:
The definition of the possible action
type
values stays unchanged.Did you notice how we've skipped the whole declaration of the action types and the union type here? Did you notice? We're directly declaring our action creators.
Notice: The
ActionType.setCounter as typeof ActionType.setCounter
construct ensures that thetype
property is of the literal type produced by the enum. Alternatively you could usetype: ActionType.setCounter = ActionType.setCounter
ortype = <ActionType.setCounter> ActionType.setCounter
. Use whatever you prefer, but don't leave off the type astype
will then be of typestring
and our discriminated unions wont work anymore.We now specify our
ActionFilter
by providing ourActionType
enumeration and our action creators (wrapped in a necessary call todeclareAction
). This is where all the type magic (okay, its only type inference) happens. The resulting object serves two purposes:- it infers the types of the actions from the action creators and provides the resulting union type in its
action
property. - its
matches(action: Redux.AnyAction) action is <Action>
method provides a typeguard to check whether an action belongs to our union. This typeguard is used by theActionReducer
to decide whether to handle the action or simply return the provided state.
- it infers the types of the actions from the action creators and provides the resulting union type in its
Our specialized reducer function looks nearly the same like before. Just notice how we type the
action
parameter withtypeof filter.action
which results in our union type.Note: Remeber to turn off tslint's switch-default rule.
Here we define our action reducer which automatically forwards to our
reduceNumberAction
for matching action objects. Afterwards we export the reducersreduce
method which is of typeRedux.Reducer
.
Not only this saved us a lot of initial code. Also lets take a look at what we save when we want to add an action:
- Add a new value to the
ActionType
enum
- ~~Define the action's type~~
- Define a creator function ~~which repeats most of the type declaration~~ which implicitly specifies the action's type
- ~~Add the new type to the union of
NumberAction
(easy to forget)~~ - ~~Add the check of the
type
property to theisNumberAction
function (easy to forget)~~ - Add the handling for this action to the reducer (the compiler will remember us to do so)
So we've saved 50% of the necessary steps and all of the steps which are easy to forget. Not bad, huh?
Bonus
The following has nothing to do with Redux actions but is nevertheless helpful for Redux TypeScript developers: This library provides a createStoreWithDevTools()
function to create your Redux store, so that it uses the redux-devtools-extension if available in your browser. Use it as a drop-in replacement for redux's createStore()
function.
API Documentation
This library comes with a full API documentation.