npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

typed-redux-actions

v2.0.1

Published

Typed internationalization (intl/i18n) library for TypeScript/JavaScript apps.

Downloads

29

Readme

npm version Build Status Coverage Status

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:

  1. 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 defining consts or using an enum like we do it here.
  2. 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 adequate ActionType.
  3. 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.
  4. 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
  5. 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.
  6. 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 the isNumberAction 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:

  1. The definition of the possible action type values stays unchanged.

  2. 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 the type property is of the literal type produced by the enum. Alternatively you could use type: ActionType.setCounter = ActionType.setCounter or type = <ActionType.setCounter> ActionType.setCounter. Use whatever you prefer, but don't leave off the type as type will then be of type string and our discriminated unions wont work anymore.

  3. We now specify our ActionFilter by providing our ActionType enumeration and our action creators (wrapped in a necessary call to declareAction). 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 the ActionReducer to decide whether to handle the action or simply return the provided state.
  4. Our specialized reducer function looks nearly the same like before. Just notice how we type the action parameter with typeof filter.action which results in our union type.

    Note: Remeber to turn off tslint's switch-default rule.

  5. Here we define our action reducer which automatically forwards to our reduceNumberAction for matching action objects. Afterwards we export the reducers reduce method which is of type Redux.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 the isNumberAction 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.