This lib is a part of React & Redux TypeScript Guide
Simple functional API that's specifically designed to reduce verbosity (no explicit type annotations!) and complexity (retain "type soundness" and easily discriminate union types of Actions) of type annotations for "Redux".
- Thoroughly tested both logic and type correctness
- No third-party dependencies
- Semantic Versioning
- Output separate bundles for different workflow needs (es5-commonjs, es5-module, jsnext)
For NPM users
$ npm install --save typesafe-actions
For Yarn users
$ yarn add typesafe-actions
While trying to use redux-actions with TypeScript I was dissapointed by it's "unsoundness" with static-typing (more info here).
Moreover alternative solutions in the wild have been either too verbose, used classes or was too "explicitly typed" instead of leveraging incredible type-inference, which just feels wrong.
That's why in typesafe-actions
I created an API specifically designed to retain "type soundness" and provide a clean functional interface without a need to type any explicit type annotations, so that it looks the same in JavaScript as in TypeScript.
To highlight the benefits of type inference leveraged in this solution, let me show you how to handle the most common use-cases found in Redux Architecture:
import { $call } from 'utility-types';
import { createAction } from 'typesafe-actions';
export const actions = {
increment: createAction('INCREMENT'),
add: createAction('ADD', (amount: number) => ({
type: 'ADD',
payload: amount,
})),
};
const returnsOfActions = Object.values(actions).map($call);
type AppAction = typeof returnsOfActions[number];
// third-party actions
type ReactRouterAction = RouterAction | LocationChangeAction;
export type RootAction =
| AppAction
| ReactRouterAction;
Use getType
function to reduce common boilerplate and discriminate union type of RootAction
to a specific action.
import { getType } from 'typesafe-actions';
import { RootState, RootAction } from '@src/redux';
import { add } from './actions';
const reducer = (state: RootState, action: RootAction) => {
switch (action.type) {
case getType(add):
return state + action.payload; // action is narrowed to a type of "add" action (payload: number)
...
Use isActionOf
function to filter actions and discriminate union type of RootAction
to a specific action(s) down the stream.
import { isActionOf } from 'typesafe-actions';
import { RootState, RootAction } from '@src/redux';
import { addTodo, toggleTodo } from './actions';
// with single action
const addTodoToast: Epic<RootAction, RootState> =
(action$, store) => action$
.filter(isActionOf(addTodo))
.concatMap((action) => { // action is asserted as: { type: "ADD_TODO", payload: string }
const toast = `Added new todo: ${action.payload}`;
...
// with multiple actions
const logTodoAction: Epic<RootAction, RootState> =
(action$, store) => action$
.filter(isActionOf([addTodo, toggleTodo]))
.switchMap((action) => { // action is asserted as: { type: "ADD_TODO", payload: string } | { type: "TOGGLE_TODO", payload: string }
const log = `Dispatched action: ${action.type}`;
...
create the action creator of a given function that contains private "type" metadata
function createAction(typeString: T, creatorFunction?: CF): CF
// CF extends (...args: any[]) => { type: T, payload?: P, meta?: M, error?: boolean }
NOTE: I know the
typeString
argument looks kinda redundand to use withcreatorFunction
and I would like to infer the type fromtype
property of returning action, but at the moment (TS v.2.6.2) it's impossible because the type inference is widening it tostring
and other API's (getType
andisActionOf
) will not work when trying to discriminate union types
This is something I should be able to address with the future TS versions and then simplify the API with backward compatibility in mind.
Examples:
// no payload
const increment = createAction('INCREMENT');
// same as:
// const increment = createAction('INCREMENT', () => ({ type: 'INCREMENT' }));
expect(increment()).toEqual({ type: 'INCREMENT' });
// with payload
const add = createAction('ADD',
(amount: number) => ({ type: 'ADD', payload: amount }),
);
expect(add(10)).toEqual({ type: 'ADD', payload: 10 });
// with payload and meta
const notify = createAction('NOTIFY',
(username: string, message: string) => ({
type: 'NOTIFY',
payload: { message: `${username}: ${message}` },
meta: { username, message },
}),
);
expect(notify('Piotr', 'Hello!'))
.toEqual({
type: 'NOTIFY',
payload: { message: 'Piotr: Hello!' },
meta: { username: 'Piotr', message: 'Hello!' },
});
get the "type" property of a given action creator
(It will infer literal type of action)
function getType(actionCreator: AC<T>): T
// AC<T> extends (...args: any[]) => { type: T }
Examples:
const increment = createAction('INCREMENT');
const type: 'INCREMENT' = getType(increment);
expect(type).toBe('INCREMENT');
// in reducer
switch (action.type) {
case getType(increment):
return state + 1;
default: return state;
}
create the assert function for specified action creator(s) - resulting function will then assert (true/false) maching actions
(it will discriminate union type to specified action creator(s))
function isActionOf(actionCreator: AC<T>): (action: any) => action is T
function isActionOf(actionCreators: Array<AC<T>>): (action: any) => action is T
// AC<T> extends (...args: any[]) => T
Examples:
// in epics, with single action
import { addTodo } from './actions';
const addTodoToast: Epic<RootAction, RootState> =
(action$, store) => action$
.filter(isActionOf(addTodo))
.concatMap((action) => { // action is asserted as: { type: "ADD_TODO", payload: string }
const toast = `Added new todo: ${action.payload}`;
...
// with multiple actions
import { addTodo, toggleTodo } from './actions';
const logTodoAction: Epic<RootAction, RootState> =
(action$, store) => action$
.filter(isActionOf([addTodo, toggleTodo]))
.concatMap((action) => { // action is asserted as: { type: "ADD_TODO", payload: string } | { type: "TOGGLE_TODO", payload: string }
const log = `Dispatched action: ${action.type}`;
...
Here you can find out the differences compared to other solutions.
tested with "@types/redux-actions": "2.2.3"
- redux-actions
const notify1 = createAction('NOTIFY')
// resulting type:
// const notify1: () => {
// type: string;
// payload: void | undefined;
// error: boolean | undefined;
// }
with
redux-actions
notice excess "nullable" propertiespayload
anderror
, also the actiontype
property is widened to string (🐼 is really sad!)
- typesafe-actions
const notify1 = createAction('NOTIFY')
// resulting type:
// const notify1: () => {
// type: "NOTIFY";
// }
with
typesafe-actions
there is no nullable types, only the data that is really there, also the action "type" property is correct narrowed to literal type (great success!)
- redux-actions
const notify2 = createAction('NOTIFY',
(username: string, message?: string) => ({
message: `${username}: ${message || ''}`
})
)
// resulting type:
// const notify2: (t1: string) => {
// type: string;
// payload: { message: string; } | undefined;
// error: boolean | undefined;
// }
notice the missing optional
message
parameter in resulting function alsousername
param name is changed tot1
, actiontype
property is widened to string and incorrect nullable properties
- typesafe-actions
const notify2 = createAction('NOTIFY',
(username: string, message?: string) => ({
type: 'NOTIFY'
payload: { message: `${username}: ${message || ''}` },
})
)
// resulting type:
// const notify2: (username: string, message?: string | undefined) => {
// type: "NOTIFY";
// payload: { message: string; };
// }
typesafe-actions
retain complete type soundness
- redux-actions
const notify3 = createAction('NOTIFY',
(username: string, message?: string) => ({ message: `${username}: ${message || ''}` }),
(username: string, message?: string) => ({ username, message })
)
// resulting type:
// const notify3: (...args: any[]) => {
// type: string;
// payload: { message: string; } | undefined;
// meta: { username: string; message: string | undefined; };
// error: boolean | undefined;
// }
notice complete loss of arguments arity and types in resulting function, moreover action
type
property is again widened to string with nullablepayload
anderror
- typesafe-actions
const notify3 = createAction('NOTIFY',
(username: string, message?: string) => ({
type: 'NOTIFY',
payload: { message: `${username}: ${message || ''}` },
meta: { username, message },
}),
)
// resulting type:
// const notify3: (username: string, message?: string | undefined) => {
// type: "NOTIFY";
// payload: { message: string; };
// meta: { username: string; message: string | undefined; };
// }
typesafe-actions
makes 🐼 happy once again
MIT License
Copyright (c) 2017 Piotr Witek piotrek.witek@gmail.com (http://piotrwitek.github.io)