This lib is a part of React & Redux TypeScript Guide
New API (link to API docs) thanks to a new language feature (conditional types) coming to TypesScript in v2.8)
Simple functional API that's specifically designed to reduce verbosity (no explicit generic type arguments in the API, type inference FTW!) and complexity (retain "type soundness" and easily discriminate union types) of type annotations.
The main goal of this library is to limit the developers using the actions to incorrectly use them by strictly checking if the input parameters are correct. (for instance when you do not provide payload creator function, this means that your action creator will not accept any parameters and show error when someone will try to do that)
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 (which hinders readability) or was too "explicitly typed" instead of leveraging incredible type-inference ("implicit types").
That's why in typesafe-actions
I created an API specifically designed to only use "type-inference" and provide a clean functional interface without a need to provide explicit generic type arguments, so that it feels like an expressive and idiomatic JavaScript.
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'; // From https://github.com/piotrwitek/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)