Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork99
Typesafe utilities for "action-creators" in Redux / Flux Architecture
License
piotrwitek/typesafe-actions
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Typesafe utilities designed to reduce typesverbosityandcomplexity in Redux Architecture.
This library is part of theReact & Redux TypeScript Guide ecosystem 📖
Found it useful? Want more updates?
Show your support by giving a ⭐


🎉Now updated to supportTypeScript v3.7 🎉
Current API Docs and Tutorial are outdated (from v4), so temporarily please use this issue asv5.x.x API Docs.
- Easily create completely typesafeActions or evenAsync Actions
- No boilerplate and completely typesafeReducers
- Game-changingHelper Types for Redux
- Todo-App playground:Codesandbox
- React, Redux, TypeScript - RealWorld App:Github |Demo
- Secure and Minimal - no third-party dependencies, according to
size-snapshot(Minified: 3.48 KB, Gzipped: 1.03 KB), check also onbundlephobia - Optimized - distribution packages bundled in 3 different formats (
cjs,esmandumd) with separate bundles for dev & prod (same asreact) - Quality - complete test-suite for an entire API surface containing regular runtime tests and extra type-tests to guaranteetype soundness and to prevent regressions in the future TypeScript versions
- Performance - integrated performance benchmarks to guarantee that the computational complexity of types are in check and there are no slow-downs when your application grow
npm run benchmark:XXX
- Installation
- Tutorial v4 (v5 is WIP #188)
- API Docs v4 (v5 is WIP #189)
- Migration Guides
- Compatibility Notes
- Recipes
- Compare to others
- Motivation
- Contributing
- Funding Issues
- License
# NPMnpm install typesafe-actions# YARNyarn add typesafe-actions
Tutorial v4 (v5 is WIP#188)
To showcase the flexibility and the power of thetype-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 andAdvanced Types.
RECOMMENDATION:
When usingtypesafe-actionsin your project you won't need to export and reusestring constants. It's becauseaction-creators created by this library have static property withaction type that you can easily access usingactions-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 ofstring constants. Check our/codesandboxapplication to learn some best-practices to create such codebase.
Limitations of TypeScript when working with string constants - when usingstring constants as actiontype property, please make sure to usesimple string literal assignment with const. This limitation is coming from the type-system, because all thedynamic 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 foraction object in reducer cases.
// Example file: './constants.ts'// WARNING: Incorrect usageexportconstADD=prefix+'ADD';// => stringexportconstADD=`${prefix}/ADD`;// => stringexportdefault{ADD:'@prefix/ADD',// => string}// Correct usageexportconstADD='@prefix/ADD';// => '@prefix/ADD'exportconstTOGGLE='@prefix/TOGGLE';// => '@prefix/TOGGLE'exportdefault({ADD:'@prefix/ADD',// => '@prefix/ADD'}asconst)// 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 whytypesafe-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 andcreateAction are creators that can createactions with predefined properties ({ type, payload, meta }). This makes them concise but also opinionated.
Important property is that resultingaction-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 thatactionWILL NOT WORK withaction-helpers.
import{action,createAction}from'typesafe-actions';exportconstadd=(title:string)=>action('todos/ADD',{id:cuid(), title,completed:false});// add: (title: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }exportconstadd=createAction('todos/ADD',action=>{// Note: "action" callback does not need "type" parameterreturn(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 withFlux Standard Action, so youraction object shape is constrained to({ type, payload, meta, error }). It is usinggeneric type arguments formeta andpayload to simplify creation of type-safe action-creators.
It is important to notice that in the resultingaction-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';exportconsttoggle=createStandardAction('todos/TOGGLE')<string>();// toggle: (payload: string) => { type: "todos/TOGGLE"; payload: string; }exportconstadd=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 onaction object to fit all the custom use-cases.
import{createCustomAction}from'typesafe-actions';constadd=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 theAPI 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';typeReactRouterAction=RouterAction|LocationChangeAction;exporttypeRootAction=|ReactRouterAction|TodosAction;
Now I want to show youaction-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 atype-guard so they'll narrowtagged union type (RootAction) to a specific action type that we want.
Instead oftype-constants we can useaction-creators instance to match specific actions in reducers and epics cases. It works by adding a static property onaction-creator instance which contains thetype string.
The most common one isgetType, which is useful for regular reducer switch cases:
switch(action.type){casegetType(todos.add):// below action type is narrowed to: { type: "todos/ADD"; payload: Todo; }return[...state,action.payload]; ...
Then we have theisActionOf helper which acceptaction-creator as first parameter matching actions with corresponding type passed as second parameter (it's a curried function).
// epics.tsimport{isActionOf}from'typesafe-actions';import{add}from'./actions';constaddTodoToast:Epic<RootAction,RootAction,RootState,Services>=(action$,state$,{ toastService})=>action$.pipe(filter(isActionOf(add)),tap(action=>{// here action type is narrowed to: { type: "todos/ADD"; payload: Todo; }toastService.success(...);})...// Works with multiple actions! (with type-safety up to 5)action$.pipe(filter(isActionOf([add,toggle]))// here action type is narrowed to a smaller union:// { type: "todos/ADD"; payload: Todo; } | { type: "todos/TOGGLE"; payload: string; }
Alternatively if your team prefers to use regulartype-constants you can still do that.
We have an equivalent helper (isOfType) which accepttype-constants as parameter providing the same functionality.
// epics.tsimport{isOfType}from'typesafe-actions';import{ADD}from'./constants';constaddTodoToast:Epic<RootAction,RootAction,RootState,Services>=(action$,state$,{ toastService})=>action$.pipe(filter(isOfType(ADD)),tap(action=>{// here action type is narrowed to: { type: "todos/ADD"; payload: Todo; } ...// Works with multiple actions! (with type-safety up to 5)action$.pipe(filter(isOfType([ADD,TOGGLE]))// here action type is narrowed to a smaller union:// { type: "todos/ADD"; payload: Todo; } | { type: "todos/TOGGLE"; payload: string; }
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 constantsif(isOfType(types.ADD,action)){// here action is narrowed to: { type: "todos/ADD"; payload: Todo; }}
We can extend internal types oftypesafe-actions module withRootAction definition of our application so that you don't need to pass generic type arguments withcreateReducer API:
// types.d.tsimport{ActionType}from'typesafe-actions';exporttypeRootAction=ActionType<typeofimport('./actions').default>;declare module'typesafe-actions'{interfaceTypes{RootAction:RootAction;}}// now you can usecreateReducer(...)// instead ofcreateReducer<State,Action>(...)
We can prevent a lot of boilerplate code and type errors using this powerful and completely typesafe API.
Using handleAction chain API:
// using action-creatorsconstcounterReducer=createReducer(0)// state and action type is automatically inferred and return type is validated to be exact type.handleAction(add,(state,action)=>state+action.payload).handleAction(add, ...// <= error is shown on duplicated or invalid actions.handleAction(increment,(state,_)=>state+1).handleAction(...// <= error is shown when all actions are handled// or handle multiple actions using array.handleAction([add,increment],(state,action)=>state+(action.type==='ADD' ?action.payload :1));// all the same scenarios are working when using type-constantsconstcounterReducer=createReducer(0).handleAction('ADD',(state,action)=>state+action.payload).handleAction('INCREMENT',(state,_)=>state+1);counterReducer(0,add(4));// => 4counterReducer(0,increment());// => 1
First we need to start by generating atagged union type of actions (TodosAction). It's very easy to do by usingActionTypetype-helper.
import{ActionType}from'typesafe-actions';import*astodosfrom'./actions';exporttypeTodosAction=ActionType<typeoftodos>;
Now we define a regular reducer function by annotatingstate andaction arguments with their respective types (TodosAction for action type).
exportdefault(state:Todo[]=[],action:TodosAction)=>{
Now in the switch cases we can use thetype property of action to narrowing the union type ofTodosAction to an action that is corresponding to that type.
switch(action.type){casegetType(add):// below action type is narrowed to: { type: "todos/ADD"; payload: Todo; }return[...state,action.payload]; ...
To handle an async-flow of http request lets implement anepic. Theepic will call a remote API using an injectedtodosApi 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 usecreateAsyncAction function providing us with a nice common interface object{ request: ... , success: ... , failure: ... } that will nicely fit with the functional API ofRxJS.This will mitigateredux verbosity and greatly reduce the maintenance cost of type annotations foractions andaction-creators that would otherwise be written explicitly.
// actions.tsimport{createAsyncAction}from'typesafe-actions';constfetchTodosAsync=createAsyncAction('FETCH_TODOS_REQUEST','FETCH_TODOS_SUCCESS','FETCH_TODOS_FAILURE','FETCH_TODOS_CANCEL')<string,Todo[],Error,string>();// epics.tsimport{fetchTodosAsync}from'./actions';constfetchTodosFlow:Epic<RootAction,RootAction,RootState,Services>=(action$,state$,{ todosApi})=>action$.pipe(filter(isActionOf(fetchTodosAsync.request)),switchMap(action=>from(todosApi.getAll(action.payload)).pipe(map(fetchTodosAsync.success),catchError((message:string)=>of(fetchTodosAsync.failure(message))),takeUntil(action$.pipe(filter(isActionOf(fetchTodosAsync.cancel)))),)));
With sagas it's not possible to achieve the same degree of type-safety as with epics because of limitations coming fromredux-saga API design.
Typescript issues:
- Typescript does not currently infer types resulting from a
yieldstatement 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.
import{createAsyncAction,createReducer}from'typesafe-actions';import{put,call,takeEvery,all}from'redux-saga/effects';// Create the set of async actionsconstfetchTodosAsync=createAsyncAction('FETCH_TODOS_REQUEST','FETCH_TODOS_SUCCESS','FETCH_TODOS_FAILURE')<string,Todo[],Error>();// Handle request sagafunction*addTodoSaga(action:ReturnType<typeoffetchTodosAsync.request>):Generator{try{constresponse:Todo[]=yieldcall(todosApi.getAll,action.payload);yieldput(fetchTodosAsync.success(response));}catch(err){yieldput(fetchTodosAsync.failure(err));}}// Main sagafunction*mainSaga(){yieldall([takeEvery(fetchTodosAsync.request,addTodoSaga),]);}// Handle success reducerexportconsttodoReducer=createReducer({}).handleAction(fetchTodosAsync.success,(state,action)=>({ ...state,todos:action.payload}));
API Docs v4 (v5 is WIP#189)
Simpleaction factory function to simplify creation of type-safe actions.
WARNING:
This approach willNOT WORK withaction-helpers (such asgetTypeandisActionOf) because it is creatingaction objects while all the other creator functions are returningenhanced action-creators.
action(type,payload?,meta?,error?)
Examples:> Advanced Usage Examples
constincrement=()=>action('INCREMENT');// { type: 'INCREMENT'; }constcreateUser=(id:number,name:string)=>action('CREATE_USER',{ id, name});// { type: 'CREATE_USER'; payload: { id: number; name: string };}constgetUsers=(params?:string)=>action('GET_USERS',undefined,params);// { type: 'GET_USERS'; meta: string | undefined; }
TIP: Starting from TypeScript v3.4 you can achieve similar results using new
as constoperator.
constincrement=()=>({type:'INCREMENT'}asconst);
Create an enhanced action-creator with unlimited number of arguments.
- Resulting action-creator will preserve semantic names of their arguments
(id, title, amount, etc...). - Returned action object have predefined properties
({ type, payload, meta })
createAction(type)createAction(type,actionCallback=>{return(namedArg1,namedArg2, ...namedArgN)=>actionCallback(payload?,meta?)})
TIP: Injected
actionCallbackargument is similar toactionAPI but doesn't need the "type" parameter
Examples:> Advanced Usage Examples
import{createAction}from'typesafe-actions';// - with type onlyconstincrement=createAction('INCREMENT');dispatch(increment());// { type: 'INCREMENT' };// - with type and payloadconstadd=createAction('ADD',action=>{return(amount:number)=>action(amount);});dispatch(add(10));// { type: 'ADD', payload: number }// - with type and metaconstgetTodos=createAction('GET_TODOS',action=>{return(params:Params)=>action(undefined,params);});dispatch(getTodos('some_meta'));// { type: 'GET_TODOS', meta: Params }// - and finally with type, payload and metaconstgetTodo=createAction('GET_TODO',action=>{return(id:string,meta:string)=>action(id,meta);});dispatch(getTodo('some_id','some_meta'));// { type: 'GET_TODO', payload: string, meta: string }
Create an enhanced action-creator compatible withFlux Standard Action to reduce boilerplate and enforce convention.
- Resulting action-creator have predefined arguments
(payload, meta) - Returned action object have predefined properties
({ type, payload, meta, error }) - But it also contains a
.map()method that allow to map(payload, meta)arguments to a custom action object({ customProp1, customProp2, ...customPropN })
createStandardAction(type)()createStandardAction(type)<TPayload,TMeta?>()createStandardAction(type).map((payload,meta)=>({ customProp1, customProp2, ...customPropN}))
TIP: Using
undefinedas generic type parameter you can make the action-creator function require NO parameters.
Examples:> Advanced Usage Examples
import{createStandardAction}from'typesafe-actions';// Very concise with use of generic type arguments// - with type onlyconstincrement=createStandardAction('INCREMENT')();constincrement=createStandardAction('INCREMENT')<undefined>();increment();// { type: 'INCREMENT' } (no parameters are required)// - with type and payloadconstadd=createStandardAction('ADD')<number>();add(10);// { type: 'ADD', payload: number }// - with type and metaconstgetData=createStandardAction('GET_DATA')<undefined,string>();getData(undefined,'meta');// { type: 'GET_DATA', meta: string }// - and finally with type, payload and metaconstgetData=createStandardAction('GET_DATA')<number,string>();getData(1,'meta');// { type: 'GET_DATA', payload: number, meta: string }// Can map payload and meta arguments to a custom action objectconstnotify=createStandardAction('NOTIFY').map((payload:string,meta:Meta)=>({from:meta.username,message:`${meta.username}:${payload}`,messageType:meta.type,datetime:newDate(),}));dispatch(notify('Hello!',{username:'Piotr',type:'announcement'}));// { type: 'NOTIFY', from: string, message: string, messageType: MessageType, datetime: Date }
Create an enhanced action-creator with unlimited number of arguments and custom properties on action object.
- Resulting action-creator will preserve semantic names of their arguments
(id, title, amount, etc...). - Returned action object have custom properties
({ type, customProp1, customProp2, ...customPropN })
createCustomAction(type,type=>{return(namedArg1,namedArg2, ...namedArgN)=>({ type, customProp1, customProp2, ...customPropN})})
Examples:> Advanced Usage Examples
import{createCustomAction}from'typesafe-actions';constadd=createCustomAction('CUSTOM',type=>{return(first:number,second:number)=>({ type,customProp1:first,customProp2:second});});dispatch(add(1));// { type: "CUSTOM"; customProp1: number; customProp2: number; }
Create an object containing three enhanced action-creators to simplify handling of async flows (e.g. network request - request/success/failure).
createAsyncAction(requestType,successType,failureType,cancelType?)<TRequestPayload,TSuccessPayload,TFailurePayload,TCancelPayload?>()
typeAsyncActionCreator<[TRequestType,TRequestPayload],[TSuccessType,TSuccessPayload],[TFailureType,TFailurePayload],[TCancelType,TCancelPayload]?>={request:StandardActionCreator<TRequestType,TRequestPayload>,success:StandardActionCreator<TSuccessType,TSuccessPayload>,failure:StandardActionCreator<TFailureType,TFailurePayload>,cancel?:StandardActionCreator<TCancelType,TCancelPayload>,}
TIP: Using
undefinedas generic type parameter you can make the action-creator function require NO parameters.
Examples:> Advanced Usage Examples
import{createAsyncAction,AsyncActionCreator}from'typesafe-actions';constfetchUsersAsync=createAsyncAction('FETCH_USERS_REQUEST','FETCH_USERS_SUCCESS','FETCH_USERS_FAILURE')<string,User[],Error>();dispatch(fetchUsersAsync.request(params));dispatch(fetchUsersAsync.success(response));dispatch(fetchUsersAsync.failure(err));constfn=(a:AsyncActionCreator<['FETCH_USERS_REQUEST',string],['FETCH_USERS_SUCCESS',User[]],['FETCH_USERS_FAILURE',Error]>)=>a;fn(fetchUsersAsync);// There is 4th optional argument to declare cancel actionconstfetchUsersAsync=createAsyncAction('FETCH_USERS_REQUEST','FETCH_USERS_SUCCESS','FETCH_USERS_FAILURE''FETCH_USERS_CANCEL')<string,User[],Error,string>();dispatch(fetchUsersAsync.cancel('reason'));constfn=(a:AsyncActionCreator<['FETCH_USERS_REQUEST',string],['FETCH_USERS_SUCCESS',User[]],['FETCH_USERS_FAILURE',Error],['FETCH_USERS_CANCEL',string]>)=>a;fn(fetchUsersAsync);
Create a typesafe reducer
createReducer<TState,TRootAction>(initialState,handlersMap?)// orcreateReducer<TState,TRootAction>(initialState).handleAction(actionCreator,reducer).handleAction([actionCreator1,actionCreator2, ...actionCreatorN],reducer).handleType(type,reducer).handleType([type1,type2, ...typeN],reducer)
Examples:> Advanced Usage Examples
TIP: You can use reducer API with atype-free syntax byExtending internal types, otherwise you'll have to pass generic type arguments like in below examples
// type-free syntax doesn't require generic type argumentsconstcounterReducer=createReducer(0,{ADD:(state,action)=>state+action.payload,[getType(increment)]:(state,_)=>state+1,})
Object map style:
import{createReducer,getType}from'typesafe-actions'typeState=number;typeAction={type:'ADD',payload:number}|{type:'INCREMENT'};constcounterReducer=createReducer<State,Action>(0,{ADD:(state,action)=>state+action.payload,[getType(increment)]:(state,_)=>state+1,})
Chain API style:
// using action-creatorsconstcounterReducer=createReducer<State,Action>(0).handleAction(add,(state,action)=>state+action.payload).handleAction(increment,(state,_)=>state+1)// handle multiple actions by using array.handleAction([add,increment],(state,action)=>state+(action.type==='ADD' ?action.payload :1));// all the same scenarios are working when using type-constantsconstcounterReducer=createReducer<State,Action>(0).handleType('ADD',(state,action)=>state+action.payload).handleType('INCREMENT',(state,_)=>state+1);
Extend or compose reducers - every operation is completely typesafe:
constnewCounterReducer=createReducer<State,Action>(0).handleAction('SUBTRACT',(state,action)=>state-action.payload).handleAction('DECREMENT',(state,_)=>state-1);constbigReducer=createReducer<State,Action>(0,{ ...counterReducer.handlers,// typesafe ...newCounterReducer.handlers,// typesafeSUBTRACT:decrementReducer.handlers.DECREMENT,// <= error, wrong type})
Get thetype property value (narrowed to literal type) of given enhanced action-creator.
getType(actionCreator)
Examples:
import{getType,createStandardAction}from'typesafe-actions';constadd=createStandardAction('ADD')<number>();// In switch reducerswitch(action.type){casegetType(add):// action type is { type: "ADD"; payload: number; }returnstate+action.payload;default:returnstate;}// or with conditional statementsif(action.type===getType(add)){// action type is { type: "ADD"; payload: number; }}
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 andaction will not work with this helper
// can be used as a binary functionisActionOf(actionCreator,action)// or as a curried functionisActionOf(actionCreator)(action)// also accepts an arrayisActionOf([actionCreator1,actionCreator2, ...actionCreatorN],action)// with its curried equivalentisActionOf([actionCreator1,actionCreator2, ...actionCreatorN])(action)
Examples:> Advanced Usage Examples
import{addTodo,removeTodo}from'./todos-actions';// Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.)// - single action[action1,action2, ...actionN].filter(isActionOf(addTodo))// only actions with type `ADD` will pass.map((action)=>{// action type is { type: "todos/ADD"; payload: Todo; } ...// - multiple actions[action1,action2, ...actionN].filter(isActionOf([addTodo,removeTodo]))// only actions with type `ADD` or 'REMOVE' will pass.do((action)=>{// action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } ...// With conditional statements// - single actionif(isActionOf(addTodo,action)){returniAcceptOnlyTodoType(action.payload);// action type is { type: "todos/ADD"; payload: Todo; }}// - multiple actionsif(isActionOf([addTodo,removeTodo],action)){returniAcceptOnlyTodoType(action.payload);// action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }}
Check if action type property is equal given type-constant(s)(it will narrow action type to a type of given action-creator(s))
// can be used as a binary functionisOfType(type,action)// or as curried functionisOfType(type)(action)// also accepts an arrayisOfType([type1,type2, ...typeN],action)// with its curried equivalentisOfType([type1,type2, ...typeN])(action)
Examples:> Advanced Usage Examples
import{ADD,REMOVE}from'./todos-types';// Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.)// - single action[action1,action2, ...actionN].filter(isOfType(ADD))// only actions with type `ADD` will pass.map((action)=>{// action type is { type: "todos/ADD"; payload: Todo; } ...// - multiple actions[action1,action2, ...actionN].filter(isOfType([ADD,REMOVE]))// only actions with type `ADD` or 'REMOVE' will pass.do((action)=>{// action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } ...// With conditional statements// - single actionif(isOfType(ADD,action)){returniAcceptOnlyTodoType(action.payload);// action type is { type: "todos/ADD"; payload: Todo; }}// - multiple actionsif(isOfType([ADD,REMOVE],action)){returniAcceptOnlyTodoType(action.payload);// action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }}
Below helper functions are very flexible generalizations, works great with nested structures and will cover numerous different use-cases.
Powerful type-helper that will infer union type fromimport * as ... oraction-creator map object.
import{ActionType}from'typesafe-actions';// with "import * as ..."import*astodosfrom'./actions';exporttypeTodosAction=ActionType<typeoftodos>;// TodosAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' }// with nested action-creator map caseconstactions={action1:createAction('action1'),nested:{action2:createAction('action2'),moreNested:{action3:createAction('action3'),},},};exporttypeRootAction=ActionType<typeofactions>;// RootAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' }
Powerful type helper that will infer state object type fromreducer function andnested/combined reducers.
WARNING: working with redux@4+ types
import{combineReducers}from'redux';import{StateType}from'typesafe-actions';// with reducer functionconsttodosReducer=(state:Todo[]=[],action:TodosAction)=>{switch(action.type){casegetType(todos.add):return[...state,action.payload]; ...exporttypeTodosState=StateType<typeoftodosReducer>;// with nested/combined reducersconstrootReducer=combineReducers({router:routerReducer,counters:countersReducer,});exporttypeRootState=StateType<typeofrootReducer>;
Breaking changes:
- In
v5all the deprecatedv4creator functions are available underdeprecatednamed import to help with incremental migration.
// beforeimport{createAction,createStandardAction,createCustomAction}from"typesafe-actions"// afterimport{deprecated}from"typesafe-actions"const{ createAction, createStandardAction, createCustomAction}=deprecated;
createStandardActionwas renamed tocreateActionand.mapmethod was removed in favor of simplerredux-actionsstyle API.
// beforeconstwithMappedPayloadAndMeta=createStandardAction('CREATE_STANDARD_ACTION').map(({ username, message}:Notification)=>({payload:`${username}:${message}`,meta:{ username, message},}));// afterconstwithMappedPayloadAndMeta=createAction('CREATE_STANDARD_ACTION',({ username, message}:Notification)=>`${username}:${message}`,// payload creator({ username, message}:Notification)=>({ username, message})// meta creator)();
v4version ofcreateActionwas removed. I suggest to refactor to use a newcreateActionas in point2, which was simplified and extended to supportredux-actionsstyle API.
// beforeconstwithPayloadAndMeta=createAction('CREATE_ACTION',resolve=>{return(id:number,token:string)=>resolve(id,token);});// afterconstwithPayloadAndMeta=createAction('CREATE_ACTION',(id:number,token:string)=>id,// payload creator(id:number,token:string)=>token// meta creator})();
createCustomAction- API was greatly simplified, now it's used like this:
// beforeconstadd=createCustomAction('CUSTOM',type=>{return(first:number,second:number)=>({ type,customProp1:first,customProp2:second});});// afterconstadd=createCustomAction('CUSTOM',(first:number,second:number)=>({customProp1:first,customProp2:second}));
AsyncActionCreatorshould be just renamed toAsyncActionCreatorBuilder.
// beforeimport{AsyncActionCreator}from"typesafe-actions"//afterimport{AsyncActionCreatorBuilder}from"typesafe-actions"
No breaking changes!
Minimal supported TypeScriptv3.1+.
Breaking changes:
createAction
- In
v2we provide acreateActionDeprecatedfunction compatible withv1createActionto help with incremental migration.
// in v1 we created action-creator like this:constgetTodo=createAction('GET_TODO',(id:string,meta:string)=>({type:'GET_TODO',payload:id,meta:meta,}));getTodo('some_id','some_meta');// { type: 'GET_TODO', payload: 'some_id', meta: 'some_meta' }// in v2 we offer few different options - please choose your preferenceconstgetTodoNoHelpers=(id:string,meta:string)=>action('GET_TODO',id,meta);constgetTodoWithHelpers=createAction('GET_TODO',action=>{return(id:string,meta:string)=>action(id,meta);});constgetTodoFSA=createStandardAction('GET_TODO')<string,string>();constgetTodoCustom=createStandardAction('GET_TODO').map(({ id, meta}:{id:string;meta:string;})=>({payload:id, meta,}));
- createAction(s)
createAction(type,payloadCreator,metaCreator)=>createStandardAction(type)()||createStandardAction(type).map(payloadMetaCreator)createActions()=>// COMING SOON!
- handleAction(s)
handleAction(type,reducer,initialState)=>createReducer(initialState).handleAction(type,reducer)handleActions(reducerMap,initialState)=>createReducer(initialState,reducerMap)
TIP: If migrating from JS -> TS, you can swap out action-creators from
redux-actionswith action-creators fromtypesafe-actionsin yourhandleActionshandlers. This works because the action-creators fromtypesafe-actionsprovide the sametoStringmethod implementation used byredux-actionsto match actions to the correct reducer.
- combineActions
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 ascore-js orreact-app-polyfill forcreate-react-app.Please check theReact guidelines on how to do that:LINKA polyfill fo IE11 is included in our/codesandbox application.
Using this recipe you can create an action creator with restricted Meta type with exact object shape.
exporttypeMetaType={analytics?:{eventName:string;};};exportconstactionWithRestrictedMeta=<Textendsstring,P>(type:T,payload:P,meta:MetaType)=>action(type,payload,meta);exportconstvalidAction=(payload:string)=>actionWithRestrictedMeta('type',payload,{analytics:{eventName:'success'}});// OK!exportconstinvalidAction=(payload:string)=>actionWithRestrictedMeta('type',payload,{analytics:{excessProp:'no way!'}});// Error// Object literal may only specify known properties, and 'excessProp' does not exist in type '{ eventName: string; }
Here you can find out a detailed comparison oftypesafe-actions to other solutions.
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)
constnotify1=createAction('NOTIFY');// resulting type:// () => {// type: string;// payload: void | undefined;// error: boolean | undefined;// }
with
redux-actionsyou can notice the redundant nullablepayloadproperty and literal type oftypeproperty is lost (discrimination of union type would not be possible)
constnotify1=()=>action('NOTIFY');// resulting type:// () => {// type: "NOTIFY";// }
with
typesafe-actionsthere is no excess nullable types and no excess properties and the action "type" property is containing a literal type
- with payload
constnotify2=createAction('NOTIFY',(username:string,message?:string)=>({message:`${username}:${message||'Empty!'}`,}));// resulting type:// (t1: string) => {// type: string;// payload: { message: string; } | undefined;// error: boolean | undefined;// }
first the optional
messageparameter is lost,usernamesemantic argument name is changed to some generict1,typeproperty is widened once again andpayloadis nullable because of broken inference
constnotify2=(username:string,message?:string)=>action('NOTIFY',{message:`${username}:${message||'Empty!'}`},);// resulting type:// (username: string, message?: string | undefined) => {// type: "NOTIFY";// payload: { message: string; };// }
typesafe-actionsinfer 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
constnotify3=createAction('NOTIFY',(username:string,message?:string)=>({message:`${username}:${message||'Empty!'}`}),(username:string,message?:string)=>({ username, message}));// resulting type:// (...args: any[]) => {// type: string;// payload: { message: string; } | undefined;// meta: { username: string; message: string | undefined; };// error: boolean | undefined;// }
this time we got a completely broken arguments arity with no type-safety because of
anytype with all the earlier issues
/** * typesafe-actions */constnotify3=(username:string,message?:string)=>action('NOTIFY',{message:`${username}:${message||'Empty!'}`},{ username, message},);// resulting type:// (username: string, message?: string | undefined) => {// type: "NOTIFY";// payload: { message: string; };// meta: { username: string; message: string | undefined; };// }
typesafe-actionsnever fail toanytype, 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 useredux-actions to reduce the maintainability cost and boilerplate ofaction-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 eithertoo verbose because of redundant type annotations (hard to maintain) orused classes (hinders readability and requires using thenew keyword 😱)
So I createdtypesafe-actions to address all of the above pain points.
The core idea was to design an API that would mostly use the power of TypeScripttype-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 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 Witekpiotrek.witek@gmail.com (http://piotrwitek.github.io)
About
Typesafe utilities for "action-creators" in Redux / Flux Architecture
Topics
Resources
License
Code of conduct
Contributing
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Packages0
Uh oh!
There was an error while loading.Please reload this page.