Gathering detailed insights and metrics for typesafe-actions
Gathering detailed insights and metrics for typesafe-actions
Gathering detailed insights and metrics for typesafe-actions
Gathering detailed insights and metrics for typesafe-actions
@tevm/actions
A typesafe library for writing forge scripts in typescript
vuex-typesafe-actions
Vuex with action creaters and type checking
typesafe-actions-reducer-builder
Reducer builder for typesafe-actions, including immer for reducer's output
@tevm/actions-types
A typesafe library for writing forge scripts in typescript
Typesafe utilities for "action-creators" in Redux / Flux Architecture
npm install typesafe-actions
Typescript
Module System
Min. Node Version
Node Version
NPM Version
TypeScript (95.77%)
JavaScript (3.36%)
HTML (0.67%)
CSS (0.2%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
2,410 Stars
347 Commits
98 Forks
14 Watchers
6 Branches
27 Contributors
Updated on Jul 04, 2025
Latest Version
5.1.0
Package Id
typesafe-actions@5.1.0
Size
54.24 kB
NPM Version
6.12.0
Node Version
10.7.0
Published on
Nov 02, 2019
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
23
Typesafe utilities designed to reduce types verbosity and complexity in Redux Architecture.
This library is part of the React & Redux TypeScript Guide ecosystem :book:
Found it useful? Want more updates?
Show your support by giving a :star:
:tada: Now updated to support TypeScript v3.7 :tada:
:warning: Library was recently updated to v5 :warning:
Current API Docs and Tutorial are outdated (from v4), so temporarily please use this issue as v5.x.x API Docs.
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
1# NPM 2npm install typesafe-actions 3 4# YARN 5yarn add typesafe-actions
To 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-actions
in 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/codesandbox
application 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.
1// Example file: './constants.ts' 2 3// WARNING: Incorrect usage 4export const ADD = prefix + 'ADD'; // => string 5export const ADD = `${prefix}/ADD`; // => string 6export default { 7 ADD: '@prefix/ADD', // => string 8} 9 10// Correct usage 11export const ADD = '@prefix/ADD'; // => '@prefix/ADD' 12export const TOGGLE = '@prefix/TOGGLE'; // => '@prefix/TOGGLE' 13export default ({ 14 ADD: '@prefix/ADD', // => '@prefix/ADD' 15} as const) // working in TS v3.4 and above => https://github.com/Microsoft/TypeScript/pull/29510
Different 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 make 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...)
.
This two creators are very similar and the only real difference is that action
WILL NOT WORK with action-helpers.
1import { action, createAction } from 'typesafe-actions'; 2 3export const add = (title: string) => action('todos/ADD', { id: cuid(), title, completed: false }); 4// add: (title: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; } 5 6export const add = createAction('todos/ADD', action => { 7 // Note: "action" callback does not need "type" parameter 8 return (title: string) => action({ id: cuid(), title, completed: false }); 9}); 10// 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-actions
in case you are migrating.
1import { createStandardAction } from 'typesafe-actions'; 2 3export const toggle = createStandardAction('todos/TOGGLE')<string>(); 4// toggle: (payload: string) => { type: "todos/TOGGLE"; payload: string; } 5 6export const add = createStandardAction('todos/ADD').map( 7 (title: string) => ({ 8 payload: { id: cuid(), title, completed: false }, 9 }) 10); 11// 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.
1import { createCustomAction } from 'typesafe-actions'; 2 3const add = createCustomAction('todos/ADD', type => { 4 return (title: string) => ({ type, id: cuid(), title, completed: false }); 5}); 6// 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 aRootAction
in 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.
1// types.d.ts 2// example of including `react-router` actions in `RootAction` 3import { RouterAction, LocationChangeAction } from 'react-router-redux'; 4import { TodosAction } from '../features/todos'; 5 6type ReactRouterAction = RouterAction | LocationChangeAction; 7 8export type RootAction = 9 | ReactRouterAction 10 | 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:
1 switch (action.type) { 2 case getType(todos.add): 3 // below action type is narrowed to: { type: "todos/ADD"; payload: Todo; } 4 return [...state, action.payload]; 5 ...
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).
1// epics.ts 2import { isActionOf } from 'typesafe-actions'; 3 4import { add } from './actions'; 5 6const addTodoToast: Epic<RootAction, RootAction, RootState, Services> = (action$, state$, { toastService }) => 7 action$.pipe( 8 filter(isActionOf(add)), 9 tap(action => { // here action type is narrowed to: { type: "todos/ADD"; payload: Todo; } 10 toastService.success(...); 11 }) 12 ... 13 14 // Works with multiple actions! (with type-safety up to 5) 15 action$.pipe( 16 filter(isActionOf([add, toggle])) // here action type is narrowed to a smaller union: 17 // { type: "todos/ADD"; payload: Todo; } | { type: "todos/TOGGLE"; payload: string; }
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.
1// epics.ts 2import { isOfType } from 'typesafe-actions'; 3 4import { ADD } from './constants'; 5 6const addTodoToast: Epic<RootAction, RootAction, RootState, Services> = (action$, state$, { toastService }) => 7 action$.pipe( 8 filter(isOfType(ADD)), 9 tap(action => { // here action type is narrowed to: { type: "todos/ADD"; payload: Todo; } 10 ... 11 12 // Works with multiple actions! (with type-safety up to 5) 13 action$.pipe( 14 filter(isOfType([ADD, TOGGLE])) // here action type is narrowed to a smaller union: 15 // { type: "todos/ADD"; payload: Todo; } | { type: "todos/TOGGLE"; payload: string; }
TIP: you can use action-helpers with other types of conditional statements.
1import { isActionOf, isOfType } from 'typesafe-actions'; 2 3if (isActionOf(actions.add, action)) { 4 // here action is narrowed to: { type: "todos/ADD"; payload: Todo; } 5} 6// or with type constants 7if (isOfType(types.ADD, action)) { 8 // here action is narrowed to: { type: "todos/ADD"; payload: Todo; } 9}
createReducer
We can extend internal types of typesafe-actions
module with RootAction
definition of our application so that you don't need to pass generic type arguments with createReducer
API:
1// types.d.ts 2import { StateType, ActionType } from 'typesafe-actions'; 3 4export type RootAction = ActionType<typeof import('./actions').default>; 5 6declare module 'typesafe-actions' { 7 interface Types { 8 RootAction: RootAction; 9 } 10} 11 12// now you can use 13createReducer(...) 14// instead of 15createReducer<State, Action>(...)
We can prevent a lot of boilerplate code and type errors using this powerfull and completely typesafe API.
Using handleAction chain API:
1// using action-creators
2const counterReducer = createReducer(0)
3 // state and action type is automatically inferred and return type is validated to be exact type
4 .handleAction(add, (state, action) => state + action.payload)
5 .handleAction(add, ... // <= error is shown on duplicated or invalid actions
6 .handleAction(increment, (state, _) => state + 1)
7 .handleAction(... // <= error is shown when all actions are handled
8
9 // or handle multiple actions using array
10 .handleAction([add, increment], (state, action) =>
11 state + (action.type === 'ADD' ? action.payload : 1)
12 );
13
14// all the same scenarios are working when using type-constants
15const counterReducer = createReducer(0)
16 .handleAction('ADD', (state, action) => state + action.payload)
17 .handleAction('INCREMENT', (state, _) => state + 1);
18
19counterReducer(0, add(4)); // => 4
20counterReducer(0, increment()); // => 1
First we need to start by generating a tagged union type of actions (TodosAction
). It's very easy to do by using ActionType
type-helper.
1import { ActionType } from 'typesafe-actions'; 2 3import * as todos from './actions'; 4export type TodosAction = ActionType<typeof todos>;
Now we define a regular reducer function by annotating state
and action
arguments with their respective types (TodosAction
for action type).
1export default (state: Todo[] = [], action: TodosAction) => {
Now in the switch cases we can use the type
property of action to narrowing the union type of TodosAction
to an action that is corresponding to that type.
1 switch (action.type) { 2 case getType(add): 3 // below action type is narrowed to: { type: "todos/ADD"; payload: Todo; } 4 return [...state, action.payload]; 5 ...
redux-observable
epicsTo handle an async-flow of http request lets implement an epic
. The epic
will call a remote API using an injected todosApi
client, which will return a Promise that we'll need to handle by using three different actions that correspond to triggering, success and failure.
To help us simplify the creation process of necessary action-creators, we'll use createAsyncAction
function providing us with a nice common interface object { request: ... , success: ... , failure: ... }
that will nicely fit with the functional API of RxJS
.
This will mitigate redux verbosity and greatly reduce the maintenance cost of type annotations for actions and action-creators that would otherwise be written explicitly.
1// actions.ts 2import { createAsyncAction } from 'typesafe-actions'; 3 4const fetchTodosAsync = createAsyncAction( 5 'FETCH_TODOS_REQUEST', 6 'FETCH_TODOS_SUCCESS', 7 'FETCH_TODOS_FAILURE', 8 'FETCH_TODOS_CANCEL' 9)<string, Todo[], Error, string>(); 10 11// epics.ts 12import { fetchTodosAsync } from './actions'; 13 14const fetchTodosFlow: Epic<RootAction, RootAction, RootState, Services> = (action$, state$, { todosApi }) => 15 action$.pipe( 16 filter(isActionOf(fetchTodosAsync.request)), 17 switchMap(action => 18 from(todosApi.getAll(action.payload)).pipe( 19 map(fetchTodosAsync.success), 20 catchError((message: string) => of(fetchTodosAsync.failure(message))), 21 takeUntil(action$.pipe(filter(isActionOf(fetchTodosAsync.cancel)))), 22 ) 23 );
redux-saga
sagasWith sagas it's not possible to achieve the same degree of type-safety as with epics because of limitations coming from redux-saga
API design.
Typescript issues:
yield
statement so you have to manually assert the type e.g. const response: Todo[] = yield call(...
Here is the latest recommendation although it's not fully optimal. If you managed to cook something better, please open an issue to share your finding with us.
1import { createAsyncAction, createReducer } from 'typesafe-actions'; 2import { put, call, takeEvery } from 'redux-saga/effetcs'; 3 4// Create the set of async actions 5const fetchTodosAsync = createAsyncAction( 6 'FETCH_TODOS_REQUEST', 7 'FETCH_TODOS_SUCCESS', 8 'FETCH_TODOS_FAILURE' 9)<string, Todo[], Error>(); 10 11// Handle request saga 12function* addTodoSaga(action: ReturnType<typeof fetchTodosAsync.request>): Generator { 13 try { 14 const response: Todo[] = yield call(todosApi.getAll, action.payload); 15 16 yield put(fetchTodosAsync.success(response)); 17 } catch (err) { 18 yield put(fetchTodosAsync.failure(err)); 19 } 20} 21 22// Main saga 23function* mainSaga() { 24 yield all([ 25 takeEvery(fetchTodosAsync.request, addTodoSaga), 26 ]); 27} 28 29// Handle success reducer 30export const todoReducer = createReducer({}) 31 .handleAction(fetchTodosAsync.success, (state, action) => ({ ...state, todos: action.payload }));
action
Simple action factory function to simplify creation of type-safe actions.
WARNING:
This approach will NOT WORK with action-helpers (such asgetType
andisActionOf
) because it is creating action objects while all the other creator functions are returning enhanced action-creators.
1action(type, payload?, meta?, error?)
Examples: > Advanced Usage Examples
1const increment = () => action('INCREMENT'); 2// { type: 'INCREMENT'; } 3 4const createUser = (id: number, name: string) => 5 action('CREATE_USER', { id, name }); 6// { type: 'CREATE_USER'; payload: { id: number; name: string }; } 7 8const getUsers = (params?: string) => 9 action('GET_USERS', undefined, params); 10// { type: 'GET_USERS'; meta: string | undefined; }
TIP: Starting from TypeScript v3.4 you can achieve similar results using new
as const
operator.
1const increment = () => ({ type: 'INCREMENT' } as const);
createAction
Create an enhanced action-creator with unlimited number of arguments.
(id, title, amount, etc...)
.({ type, payload, meta })
1createAction(type)
2createAction(type, actionCallback => {
3 return (namedArg1, namedArg2, ...namedArgN) => actionCallback(payload?, meta?)
4})
TIP: Injected
actionCallback
argument is similar toaction
API but doesn't need the "type" parameter
Examples: > Advanced Usage Examples
1import { createAction } from 'typesafe-actions'; 2 3// - with type only 4const increment = createAction('INCREMENT'); 5dispatch(increment()); 6// { type: 'INCREMENT' }; 7 8// - with type and payload 9const add = createAction('ADD', action => { 10 return (amount: number) => action(amount); 11}); 12dispatch(add(10)); 13// { type: 'ADD', payload: number } 14 15// - with type and meta 16const getTodos = createAction('GET_TODOS', action => { 17 return (params: Params) => action(undefined, params); 18}); 19dispatch(getTodos('some_meta')); 20// { type: 'GET_TODOS', meta: Params } 21 22// - and finally with type, payload and meta 23const getTodo = createAction('GET_TODO', action => { 24 return (id: string, meta: string) => action(id, meta); 25}); 26dispatch(getTodo('some_id', 'some_meta')); 27// { type: 'GET_TODO', payload: string, meta: string }
createStandardAction
Create an enhanced action-creator compatible with Flux Standard Action to reduce boilerplate and enforce convention.
(payload, meta)
({ type, payload, meta, error })
.map()
method that allow to map (payload, meta)
arguments to a custom action object ({ customProp1, customProp2, ...customPropN })
1createStandardAction(type)() 2createStandardAction(type)<TPayload, TMeta?>() 3createStandardAction(type).map((payload, meta) => ({ customProp1, customProp2, ...customPropN }))
TIP: Using
undefined
as generic type parameter you can make the action-creator function require NO parameters.
Examples: > Advanced Usage Examples
1import { createStandardAction } from 'typesafe-actions'; 2 3// Very concise with use of generic type arguments 4// - with type only 5const increment = createStandardAction('INCREMENT')(); 6const increment = createStandardAction('INCREMENT')<undefined>(); 7increment(); // { type: 'INCREMENT' } (no parameters are required) 8 9 10// - with type and payload 11const add = createStandardAction('ADD')<number>(); 12add(10); // { type: 'ADD', payload: number } 13 14// - with type and meta 15const getData = createStandardAction('GET_DATA')<undefined, string>(); 16getData(undefined, 'meta'); // { type: 'GET_DATA', meta: string } 17 18// - and finally with type, payload and meta 19const getData = createStandardAction('GET_DATA')<number, string>(); 20getData(1, 'meta'); // { type: 'GET_DATA', payload: number, meta: string } 21 22// Can map payload and meta arguments to a custom action object 23const notify = createStandardAction('NOTIFY').map( 24 (payload: string, meta: Meta) => ({ 25 from: meta.username, 26 message: `${username}: ${payload}`, 27 messageType: meta.type, 28 datetime: new Date(), 29 }) 30); 31 32dispatch(notify('Hello!', { username: 'Piotr', type: 'announcement' })); 33// { type: 'NOTIFY', from: string, message: string, messageType: MessageType, datetime: Date }
createCustomAction
Create an enhanced action-creator with unlimited number of arguments and custom properties on action object.
(id, title, amount, etc...)
.({ type, customProp1, customProp2, ...customPropN })
1createCustomAction(type, type => {
2 return (namedArg1, namedArg2, ...namedArgN) => ({ type, customProp1, customProp2, ...customPropN })
3})
Examples: > Advanced Usage Examples
1import { createCustomAction } from 'typesafe-actions'; 2 3const add = createCustomAction('CUSTOM', type => { 4 return (first: number, second: number) => ({ type, customProp1: first, customProp2: second }); 5}); 6 7dispatch(add(1)); 8// { type: "CUSTOM"; customProp1: number; customProp2: number; }
createAsyncAction
Create an object containing three enhanced action-creators to simplify handling of async flows (e.g. network request - request/success/failure).
1createAsyncAction(
2 requestType, successType, failureType, cancelType?
3)<TRequestPayload, TSuccessPayload, TFailurePayload, TCancelPayload?>()
AsyncActionCreator
1type AsyncActionCreator< 2 [TRequestType, TRequestPayload], 3 [TSuccessType, TSuccessPayload], 4 [TFailureType, TFailurePayload], 5 [TCancelType, TCancelPayload]? 6> = { 7 request: StandardActionCreator<TRequestType, TRequestPayload>, 8 success: StandardActionCreator<TSuccessType, TSuccessPayload>, 9 failure: StandardActionCreator<TFailureType, TFailurePayload>, 10 cancel?: StandardActionCreator<TCancelType, TCancelPayload>, 11}
TIP: Using
undefined
as generic type parameter you can make the action-creator function require NO parameters.
Examples: > Advanced Usage Examples
1import { createAsyncAction, AsyncActionCreator } from 'typesafe-actions'; 2 3const fetchUsersAsync = createAsyncAction( 4 'FETCH_USERS_REQUEST', 5 'FETCH_USERS_SUCCESS', 6 'FETCH_USERS_FAILURE' 7)<string, User[], Error>(); 8 9dispatch(fetchUsersAsync.request(params)); 10 11dispatch(fetchUsersAsync.success(response)); 12 13dispatch(fetchUsersAsync.failure(err)); 14 15const fn = ( 16 a: AsyncActionCreator< 17 ['FETCH_USERS_REQUEST', string], 18 ['FETCH_USERS_SUCCESS', User[]], 19 ['FETCH_USERS_FAILURE', Error] 20 > 21) => a; 22fn(fetchUsersAsync); 23 24// There is 4th optional argument to declare cancel action 25const fetchUsersAsync = createAsyncAction( 26 'FETCH_USERS_REQUEST', 27 'FETCH_USERS_SUCCESS', 28 'FETCH_USERS_FAILURE' 29 'FETCH_USERS_CANCEL' 30)<string, User[], Error, string>(); 31 32dispatch(fetchUsersAsync.cancel('reason')); 33 34const fn = ( 35 a: AsyncActionCreator< 36 ['FETCH_USERS_REQUEST', string], 37 ['FETCH_USERS_SUCCESS', User[]], 38 ['FETCH_USERS_FAILURE', Error], 39 ['FETCH_USERS_CANCEL', string] 40 > 41) => a; 42fn(fetchUsersAsync);
createReducer
Create a typesafe reducer
1createReducer<TState, TRootAction>(initialState, handlersMap?)
2// or
3createReducer<TState, TRootAction>(initialState)
4 .handleAction(actionCreator, reducer)
5 .handleAction([actionCreator1, actionCreator2, ...actionCreatorN], reducer)
6 .handleType(type, reducer)
7 .handleType([type1, type2, ...typeN], reducer)
Examples: > Advanced Usage Examples
TIP: You can use reducer API with a type-free syntax by Extending internal types, otherwise you'll have to pass generic type arguments like in below examples
1// type-free syntax doesn't require generic type arguments 2const counterReducer = createReducer(0, { 3 ADD: (state, action) => state + action.payload, 4 [getType(increment)]: (state, _) => state + 1, 5})
Object map style:
1import { createReducer, getType } from 'typesafe-actions' 2 3type State = number; 4type Action = { type: 'ADD', payload: number } | { type: 'INCREMENT' }; 5 6const counterReducer = createReducer<State, Action>(0, { 7 ADD: (state, action) => state + action.payload, 8 [getType(increment)]: (state, _) => state + 1, 9})
Chain API style:
1// using action-creators 2const counterReducer = createReducer<State, Action>(0) 3 .handleAction(add, (state, action) => state + action.payload) 4 .handleAction(increment, (state, _) => state + 1) 5 6 // handle multiple actions by using array 7 .handleAction([add, increment], (state, action) => 8 state + (action.type === 'ADD' ? action.payload : 1) 9 ); 10 11// all the same scenarios are working when using type-constants 12const counterReducer = createReducer<State, Action>(0) 13 .handleType('ADD', (state, action) => state + action.payload) 14 .handleType('INCREMENT', (state, _) => state + 1);
Extend or compose reducers - every operation is completely typesafe:
1const newCounterReducer = createReducer<State, Action>(0) 2 .handleAction('SUBTRACT', (state, action) => state - action.payload) 3 .handleAction('DECREMENT', (state, _) => state - 1); 4 5const bigReducer = createReducer<State, Action>(0, { 6 ...counterReducer.handlers, // typesafe 7 ...newCounterReducer.handlers, // typesafe 8 SUBTRACT: decrementReducer.handlers.DECREMENT, // <= error, wrong type 9})
getType
Get the type property value (narrowed to literal type) of given enhanced action-creator.
1getType(actionCreator)
Examples:
1import { getType, createStandardAction } from 'typesafe-actions'; 2 3const add = createStandardAction('ADD')<number>(); 4 5// In switch reducer 6switch (action.type) { 7 case getType(add): 8 // action type is { type: "ADD"; payload: number; } 9 return state + action.payload; 10 11 default: 12 return state; 13} 14 15// or with conditional statements 16if (action.type === getType(add)) { 17 // action type is { type: "ADD"; payload: number; } 18}
isActionOf
Check if action is an instance of given enhanced action-creator(s) (it will narrow action type to a type of given action-creator(s))
WARNING: Regular action creators and action will not work with this helper
1// can be used as a binary function
2isActionOf(actionCreator, action)
3// or as a curried function
4isActionOf(actionCreator)(action)
5// also accepts an array
6isActionOf([actionCreator1, actionCreator2, ...actionCreatorN], action)
7// with its curried equivalent
8isActionOf([actionCreator1, actionCreator2, ...actionCreatorN])(action)
Examples: > Advanced Usage Examples
1import { addTodo, removeTodo } from './todos-actions'; 2 3// Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.) 4// - single action 5[action1, action2, ...actionN] 6 .filter(isActionOf(addTodo)) // only actions with type `ADD` will pass 7 .map((action) => { 8 // action type is { type: "todos/ADD"; payload: Todo; } 9 ... 10 11// - multiple actions 12[action1, action2, ...actionN] 13 .filter(isActionOf([addTodo, removeTodo])) // only actions with type `ADD` or 'REMOVE' will pass 14 .do((action) => { 15 // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } 16 ... 17 18// With conditional statements 19// - single action 20if(isActionOf(addTodo, action)) { 21 return iAcceptOnlyTodoType(action.payload); 22 // action type is { type: "todos/ADD"; payload: Todo; } 23} 24// - multiple actions 25if(isActionOf([addTodo, removeTodo], action)) { 26 return iAcceptOnlyTodoType(action.payload); 27 // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } 28}
isOfType
Check if action type property is equal given type-constant(s) (it will narrow action type to a type of given action-creator(s))
1// can be used as a binary function
2isOfType(type, action)
3// or as curried function
4isOfType(type)(action)
5// also accepts an array
6isOfType([type1, type2, ...typeN], action)
7// with its curried equivalent
8isOfType([type1, type2, ...typeN])(action)
Examples: > Advanced Usage Examples
1import { ADD, REMOVE } from './todos-types'; 2 3// Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.) 4// - single action 5[action1, action2, ...actionN] 6 .filter(isOfType(ADD)) // only actions with type `ADD` will pass 7 .map((action) => { 8 // action type is { type: "todos/ADD"; payload: Todo; } 9 ... 10 11// - multiple actions 12[action1, action2, ...actionN] 13 .filter(isOfType([ADD, REMOVE])) // only actions with type `ADD` or 'REMOVE' will pass 14 .do((action) => { 15 // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } 16 ... 17 18// With conditional statements 19// - single action 20if(isOfType(ADD, action)) { 21 return iAcceptOnlyTodoType(action.payload); 22 // action type is { type: "todos/ADD"; payload: Todo; } 23} 24// - multiple actions 25if(isOfType([ADD, REMOVE], action)) { 26 return iAcceptOnlyTodoType(action.payload); 27 // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } 28}
Below helper functions are very flexible generalizations, works great with nested structures and will cover numerous different use-cases.
ActionType
Powerful type-helper that will infer union type from import * as ... or action-creator map object.
1import { ActionType } from 'typesafe-actions'; 2 3// with "import * as ..." 4import * as todos from './actions'; 5export type TodosAction = ActionType<typeof todos>; 6// TodosAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' } 7 8// with nested action-creator map case 9const actions = { 10 action1: createAction('action1'), 11 nested: { 12 action2: createAction('action2'), 13 moreNested: { 14 action3: createAction('action3'), 15 }, 16 }, 17}; 18export type RootAction = ActionType<typeof actions>; 19// RootAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' }
StateType
Powerful type helper that will infer state object type from reducer function and nested/combined reducers.
WARNING: working with redux@4+ types
1import { combineReducers } from 'redux'; 2import { StateType } from 'typesafe-actions'; 3 4// with reducer function 5const todosReducer = (state: Todo[] = [], action: TodosAction) => { 6 switch (action.type) { 7 case getType(todos.add): 8 return [...state, action.payload]; 9 ... 10export type TodosState = StateType<typeof todosReducer>; 11 12// with nested/combined reducers 13const rootReducer = combineReducers({ 14 router: routerReducer, 15 counters: countersReducer, 16}); 17export type RootState = StateType<typeof rootReducer>;
v4.x.x
to v5.x.x
Breaking changes:
v5
all the deprecated v4
creator functions are available under deprecated
named import to help with incremental migration.1// before 2import { createAction, createStandardAction, createCustomAction } from "typesafe-actions" 3 4// after 5import { deprecated } from "typesafe-actions" 6const { createAction, createStandardAction, createCustomAction } = deprecated;
createStandardAction
was renamed to createAction
and .map
method was removed in favor of simpler redux-actions
style API.1// before 2const withMappedPayloadAndMeta = createStandardAction( 3 'CREATE_STANDARD_ACTION' 4).map(({ username, message }: Notification) => ({ 5 payload: `${username}: ${message}`, 6 meta: { username, message }, 7})); 8 9// after 10const withMappedPayloadAndMeta = createAction( 11 'CREATE_STANDARD_ACTION', 12 ({ username, message }: Notification) => `${username}: ${message}`, // payload creator 13 ({ username, message }: Notification) => ({ username, message }) // meta creator 14)();
v4
version of createAction
was removed. I suggest to refactor to use a new createAction
as in point 2
, which was simplified and extended to support redux-actions
style API.1// before 2const withPayloadAndMeta = createAction('CREATE_ACTION', resolve => { 3 return (id: number, token: string) => resolve(id, token); 4}); 5 6// after 7const withPayloadAndMeta = createAction( 8 'CREATE_ACTION', 9 (id: number, token: string) => id, // payload creator 10 (id: number, token: string) => token // meta creator 11})();
createCustomAction
- API was greatly simplified, now it's used like this:1// before 2const add = createCustomAction('CUSTOM', type => { 3 return (first: number, second: number) => ({ type, customProp1: first, customProp2: second }); 4}); 5 6// after 7const add = createCustomAction( 8 'CUSTOM', 9 (first: number, second: number) => ({ customProp1: first, customProp2: second }) 10);
AsyncActionCreator
should be just renamed to AsyncActionCreatorBuilder
.1// before 2import { AsyncActionCreator } from "typesafe-actions" 3 4//after 5import { AsyncActionCreatorBuilder } from "typesafe-actions"
v3.x.x
to v4.x.x
No breaking changes!
v2.x.x
to v3.x.x
Minimal supported TypeScript v3.1+
.
v1.x.x
to v2.x.x
Breaking changes:
createAction
v2
we provide a createActionDeprecated
function compatible with v1
createAction
to help with incremental migration.1// in v1 we created action-creator like this: 2const getTodo = createAction('GET_TODO', 3 (id: string, meta: string) => ({ 4 type: 'GET_TODO', 5 payload: id, 6 meta: meta, 7 }) 8); 9 10getTodo('some_id', 'some_meta'); // { type: 'GET_TODO', payload: 'some_id', meta: 'some_meta' } 11 12// in v2 we offer few different options - please choose your preference 13const getTodoNoHelpers = (id: string, meta: string) => action('GET_TODO', id, meta); 14 15const getTodoWithHelpers = createAction('GET_TODO', action => { 16 return (id: string, meta: string) => action(id, meta); 17}); 18 19const getTodoFSA = createStandardAction('GET_TODO')<string, string>(); 20 21const getTodoCustom = createStandardAction('GET_TODO').map( 22 ({ id, meta }: { id: string; meta: string; }) => ({ 23 payload: id, 24 meta, 25 }) 26);
redux-actions
to typesafe-actions
1createAction(type, payloadCreator, metaCreator) => createStandardAction(type)() || createStandardAction(type).map(payloadMetaCreator)
2
3createActions() => // COMING SOON!
1handleAction(type, reducer, initialState) => createReducer(initialState).handleAction(type, reducer)
2
3handleActions(reducerMap, initialState) => createReducer(initialState, reducerMap)
TIP: If migrating from JS -> TS, you can swap out action-creators from
redux-actions
with action-creators fromtypesafe-actions
in yourhandleActions
handlers. This works because the action-creators fromtypesafe-actions
provide the sametoString
method implementation used byredux-actions
to match actions to the correct reducer.
Not needed because each function in the API accept single value or array of values for action types or action creators.
TypeScript support
5.X.X
- TypeScript v3.2+4.X.X
- TypeScript v3.2+3.X.X
- TypeScript v3.2+2.X.X
- TypeScript v2.9+1.X.X
- TypeScript v2.7+Browser support
It's compatible with all modern browsers.
For older browsers support (e.g. IE <= 11) and some mobile devices you need to provide the following polyfills:
Recommended polyfill for IE
To provide the best compatibility please include a popular polyfill package in your application, such as core-js
or react-app-polyfill
for create-react-app
.
Please check the React
guidelines to learn how to do that: LINK
A polyfill fo IE11 is included in our /codesandbox
application.
action
creatorUsing this recipe you can create an action creator with restricted Meta type with exact object shape.
1export type MetaType = {
2 analytics?: {
3 eventName: string;
4 };
5};
6
7export const actionWithRestrictedMeta = <T extends string, P>(
8 type: T,
9 payload: P,
10 meta: MetaType
11) => action(type, payload, meta);
12
13export const validAction = (payload: string) =>
14 actionWithRestrictedMeta('type', payload, { analytics: { eventName: 'success' } }); // OK!
15
16export const invalidAction = (payload: string) =>
17 actionWithRestrictedMeta('type', payload, { analytics: { excessProp: 'no way!' } }); // Error
18// Object literal may only specify known properties, and 'excessProp' does not exist in type '{ eventName: string; }
Here you can find out a detailed comparison of typesafe-actions
to other solutions.
redux-actions
Lets compare the 3 most common variants of action-creators (with type only, with payload and with payload + meta)
Note: tested with "@types/redux-actions": "2.2.3"
- with type only (no payload)
1const notify1 = createAction('NOTIFY'); 2// resulting type: 3// () => { 4// type: string; 5// payload: void | undefined; 6// error: boolean | undefined; 7// }
with
redux-actions
you can notice the redundant nullablepayload
property and literal type oftype
property is lost (discrimination of union type would not be possible)
1const notify1 = () => action('NOTIFY'); 2// resulting type: 3// () => { 4// type: "NOTIFY"; 5// }
with
typesafe-actions
there is no excess nullable types and no excess properties and the action "type" property is containing a literal type
- with payload
1const notify2 = createAction('NOTIFY', 2 (username: string, message?: string) => ({ 3 message: `${username}: ${message || 'Empty!'}`, 4 }) 5); 6// resulting type: 7// (t1: string) => { 8// type: string; 9// payload: { message: string; } | undefined; 10// error: boolean | undefined; 11// }
first the optional
message
parameter is lost,username
semantic argument name is changed to some generict1
,type
property is widened once again andpayload
is nullable because of broken inference
1const notify2 = (username: string, message?: string) => action( 2 'NOTIFY', 3 { message: `${username}: ${message || 'Empty!'}` }, 4); 5// resulting type: 6// (username: string, message?: string | undefined) => { 7// type: "NOTIFY"; 8// payload: { message: string; }; 9// }
typesafe-actions
infer very precise resulting type, notice working optional parameters and semantic argument names are preserved which is really important for great intellisense experience
- with payload and meta
1const notify3 = createAction('NOTIFY', 2 (username: string, message?: string) => ( 3 { message: `${username}: ${message || 'Empty!'}` } 4 ), 5 (username: string, message?: string) => ( 6 { username, message } 7 ) 8); 9// resulting type: 10// (...args: any[]) => { 11// type: string; 12// payload: { message: string; } | undefined; 13// meta: { username: string; message: string | undefined; }; 14// error: boolean | undefined; 15// }
this time we got a completely broken arguments arity with no type-safety because of
any
type with all the earlier issues
1/** 2 * typesafe-actions 3 */ 4const notify3 = (username: string, message?: string) => action( 5 'NOTIFY', 6 { message: `${username}: ${message || 'Empty!'}` }, 7 { username, message }, 8); 9// resulting type: 10// (username: string, message?: string | undefined) => { 11// type: "NOTIFY"; 12// payload: { message: string; }; 13// meta: { username: string; message: string | undefined; }; 14// }
typesafe-actions
never fail toany
type, even with this advanced scenario all types are correct and provide complete type-safety and excellent developer experience
When I started to combine Redux with TypeScript, I was trying to use redux-actions to reduce the maintainability cost and boilerplate of action-creators. Unfortunately, the results were intimidating: incorrect type signatures and broken type-inference cascading throughout the entire code-base (click here for a detailed comparison).
Existing solutions in the wild have been either too verbose because of redundant type annotations (hard to maintain) or used classes (hinders readability and requires using the new keyword ????)
So I created typesafe-actions
to address all of the above pain points.
The core idea was to design an API that would mostly use the power of TypeScript type-inference ???? to lift the "maintainability burden" of type annotations. In addition, I wanted to make it "look and feel" as close as possible to the idiomatic JavaScript ❤️ , so we don't have to write the redundant type annotations that which will create additional noise in your code.
You can help make this project better by contributing. If you're planning to contribute please make sure to check our contributing guide: CONTRIBUTING.md
You can also help by funding issues. Issues like bug fixes or feature requests can be very quickly resolved when funded through the IssueHunt platform.
I highly recommend to add a bounty to the issue that you're waiting for to increase priority and attract contributors willing to work on it.
Copyright (c) 2017 Piotr Witek piotrek.witek@gmail.com (http://piotrwitek.github.io)
No vulnerabilities found.
Reason
security policy file detected
Details
Reason
no binaries found in the repo
Reason
license file detected
Details
Reason
Found 9/30 approved changesets -- score normalized to 3
Reason
0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
Reason
branch protection not enabled on development/release branches
Details
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
Reason
150 existing vulnerabilities detected
Details
Score
Last Scanned on 2025-06-30
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More