Typesafe utilities for "action-creators" in Redux / Flux Architecture
Typesafe utilities for "action-creators" in Redux / Flux Architecture
size-snapshot (Minified: 3.48 KB, Gzipped: 1.03 KB), check also on bundlephobiacjs, esm and umd) with separate bundles for dev & prod (same as react)npm run benchmark:XXX# NPM
npm install typesafe-actions
# YARN
yarn add typesafe-actionsTo showcase the flexibility and the power of the type-safety provided by this library, let's build the most common parts of a typical todo-app using a Redux architecture:
WARNING
Please make sure that you are familiar with the following concepts of programming languages to be able to follow along: Type Inference, Control flow analysis, Tagged union types, Generics and Advanced Types.
RECOMMENDATION:
When usingtypesafe-actionsin your project you won't need to export and reuse string constants. It's because action-creators created by this library have static property with action type that you can easily access using actions-helpers and then use it in reducers, epics, sagas, and basically any other place. This will simplify your codebase and remove some boilerplate code associated with the usage of string constants. Check our/codesandboxapplication to learn some best-practices to create such codebase.
Limitations of TypeScript when working with string constants - when using string constants as action type property, please make sure to use simple string literal assignment with const. This limitation is coming from the type-system, because all the dynamic string operations (e.g. string concatenation, template strings and also object used as a map) will widen the literal type to its super-type, string. As a result this will break contextual typing for action object in reducer cases.
// Example file: './constants.ts'
// WARNING: Incorrect usage
export const ADD = prefix + 'ADD'; // => string
export const ADD = `${prefix}/ADD`; // => string
export default {
ADD: '@prefix/ADD', // => string
}
// Correct usage
export const ADD = '@prefix/ADD'; // => '@prefix/ADD'
export const TOGGLE = '@prefix/TOGGLE'; // => '@prefix/TOGGLE'
export default ({
ADD: '@prefix/ADD', // => '@prefix/ADD'
} as const) // working in TS v3.4 and above => https://github.com/Microsoft/TypeScript/pull/29510Different projects have different needs, and conventions vary across teams, and this is why typesafe-actions was designed with flexibility in mind. It provides three different major styles so you can choose whichever would be the best fit for your team.
action and createAction are creators that can create actions with predefined properties ({ type, payload, meta }). This makes them concise but also opinionated.
Important property is that resulting action-creator will have a variadic number of arguments and preserve their semantic names (id, title, amount, etc...).
These two creators are very similar and the only real difference is that action WILL NOT WORK with action-helpers.
import { action, createAction } from 'typesafe-actions';
export const add = (title: string) => action('todos/ADD', { id: cuid(), title, completed: false });
// add: (title: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }
export const add = createAction('todos/ADD', action => {
// Note: "action" callback does not need "type" parameter
return (title: string) => action({ id: cuid(), title, completed: false });
});
// add: (title: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }This style is aligned with Flux Standard Action, so your action object shape is constrained to ({ type, payload, meta, error }). It is using generic type arguments for meta and payload to simplify creation of type-safe action-creators.
It is important to notice that in the resulting action-creator arguments are also constrained to the predefined: (payload, meta), making it the most opinionated creator.
TIP: This creator is the most compatible with
redux-actionsin case you are migrating.
import { createStandardAction } from 'typesafe-actions';
export const toggle = createStandardAction('todos/TOGGLE')<string>();
// toggle: (payload: string) => { type: "todos/TOGGLE"; payload: string; }
export const add = createStandardAction('todos/ADD').map(
(title: string) => ({
payload: { id: cuid(), title, completed: false },
})
);
// add: (payload: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }This approach will give us the most flexibility of all creators, providing a variadic number of named parameters and custom properties on action object to fit all the custom use-cases.
import { createCustomAction } from 'typesafe-actions';
const add = createCustomAction('todos/ADD', type => {
return (title: string) => ({ type, id: cuid(), title, completed: false });
});
// add: (title: string) => { type: "todos/ADD"; id: string; title: string; completed: boolean; }TIP: For more examples please check the API Docs.
RECOMMENDATION
Common approach is to create aRootActionin the central point of your redux store - it will represent all possible action types in your application. You can even merge it with third-party action types as shown below to make your model complete.
// types.d.ts
// example of including `react-router` actions in `RootAction`
import { RouterAction, LocationChangeAction } from 'react-router-redux';
import { TodosAction } from '../features/todos';
type ReactRouterAction = RouterAction | LocationChangeAction;
export type RootAction =
| ReactRouterAction
| TodosAction;Now I want to show you action-helpers and explain their use-cases. We're going to implement a side-effect responsible for showing a success toast when user adds a new todo.
Important thing to notice is that all these helpers are acting as a type-guard so they'll narrow tagged union type (RootAction) to a specific action type that we want.
Instead of type-constants we can use action-creators instance to match specific actions in reducers and epics cases. It works by adding a static property on action-creator instance which contains the type string.
The most common one is getType, which is useful for regular reducer switch cases:
switch (action.type) {
case getType(todos.add):
// below action type is narrowed to: { type: "todos/ADD"; payload: Todo; }
return [...state, action.payload];
...Then we have the isActionOf helper which accept action-creator as first parameter matching actions with corresponding type passed as second parameter (it's a curried function).
…Alternatively if your team prefers to use regular type-constants you can still do that.
We have an equivalent helper (isOfType) which accept type-constants as parameter providing the same functionality.
…TIP: you can use action-helpers with other types of conditional statements.
import { isActionOf, isOfType } from 'typesafe-actions';
if (isActionOf(actions.add, action)) {
// here action is narrowed to: { type: "todos/ADD"; payload: Todo; }
}
// or with type constants
if (isOfType(types.ADD, action)) {
//No open issues yet, or sync has not completed.