Support for typed Actions
razjel opened this issue · 14 comments
I'm using Redux alongside with TypeScript and I created my custom class Action
and ActionCreator
//Action.ts
export default class Action
{
public type:string;
public data:any;
constructor(type:string, data:any = null)
{
this.type = type;
this.data = data;
}
}
// PhotoActions.ts
export function processPhoto(photoData:PhotoData)
{
return new Action(PROCESS_PHOTO, photoData);
}
When I try to dispatch this action I get error Actions must be plain objects. Use custom middleware for async actions.
Is it really necessary for action objects to be plain objects?
Yeah, record/replay and associated features won't work with an action class.
We don't want to encourage this so people don't lock themselves out of great features.
Does TypeScript support discriminated unions or something?
You could define Action
as an object whose shape depends on type
.
Related: flux standard action
TypeScript has discriminated unions, so when I get some free time I will try to figure something out with it.
@cesarandreu is this flux standard action
also related to actions in redux? Because in my actions instead of payload
I name property data
, will it break something?
Ok, I figured it out, I create plain object and cast it as my desired class, thanks to this I can get syntax checking with plain objects:
// PhotoActions.ts
export function processPhoto(photoData:PhotoData)
{
return {
type: PROCESS_PHOTO,
data: photoData
} as Action;
}
is this flux standard action also related to actions in redux?
It's just a recommendation. You can follow it or ignore it.
Closing as the initial issue appears to be solved by casting.
(Discriminated unions should indeed be a better solution.)
I am also using Redux with Typescript and trying to get some static typing guarantees on my actions.
Typescript supports union types, but doesn't support discriminated unions, so a solution based on that is a no go.
What I arrived at was something that looked like this:
ActionTypes.ts
export abstract class Action {
// (For maximum compatibility with redux-logger and redux-devtools)
get type(): string {
return this.constructor.toString().match(/\w+/g)[1];
}
}
export class UserInfoRequest extends Action { }
export class UserInfoSuccess extends Action {
constructor(public data: any) { super(); }
}
Actions.ts
...
function loadUsersSuccess(responseData: any) : Object {
return new ActionTypes.UserInfoSuccess(responseData.data);
}
...
UserManagementReducer.ts
...
if (action instanceof UserInfoSuccess) {
// here I now get compile-time errors if I try to access
// a nonexistent property of UserInfoSuccess (e.g. dtaa). Success!
return convertIncomingUserData(action.data);
} else {
return state;
}
...
The only problem is that I get the error Actions must be plain objects. Use custom middleware for async actions
. Up above you say this is because otherwise it will break record/replay functionality. But this is actually still completely intact (as far as I can tell). When I add redux-logger and the devtools, I can still cancel the load action, see the data disappear, then replay it and see the data appear again. It all seems to work completely normally.
I would really like to be able to leverage the advantages of both Typescript and Redux. Any idea what is going on here? Is the 'plain object' check perhaps just too strict?
Can you try using discriminated unions instead of classes?
Using class
and instanceof
seems like the wrong way to achieve type safety here.
As far as I know Typescript does not support discriminated unions.
It supports union types, like these: http://blogs.msdn.com/b/typescript/archive/2014/11/18/what-s-new-in-the-typescript-type-system.aspx?PageIndex=3
But not discriminated unions / sum types: microsoft/TypeScript#186
As far as 'the wrong way to achieve type safety' goes - if discriminated unions were available I would use them! But we work with what we have, and this approach does give me type safety and compile-time checks.
(realized I left it out of my initial post)
I also did try the casting solution and it does not work - even though plain objects can be casted to my defined classes the reducer still ends up receiving them as plain objects, so I cannot get a combination of compile-time safety with working runtime behaviour. I am guessing razjel was still switching on the type
property in the reducer and didn't have compile-time checks on properties as a goal.
See a workaround for TypeScript 1.6: #992 (comment)
There is another approach for action typing: #992 (comment)
See the workthrough by douglas. Really an awesome work!
I am sorry, action can only be plain objects. So this is not the solution.
@aikoven 's solution seem's great!
Yeah, record/replay and associated features won't work with an action class.
In fact, it works with "Redux DevTools" chrome extension, while it fails with subj exception in "clean" chrome.
My solution so far is to wrap dispatch into a function (action) => dispatch({...action})
which is better than changing a code from "new Action()" to "{} as IAction"
We don't want to encourage this
The restriction encouraged me to create a hack around it
++ ok, i have read the other thread, and then i read the thread with a guy who wants to put functions into action, and i see that this is exactly the thing you did not want people to do.
About the "legitimate" request to relax a constraint - between having an interface plus function to create a plain object and having a class the difference is that there are considerably less code for class.
export interface IMyAction extends IAction
{
content: string;
}
export const createMyAction = (content: string) => {
return {
type: "MY_ACTION",
content: content
}
}
vs
export class MyAction implements IAction {
public type: string = "MY_ACTION";
constructor(public content: string ){}
}
On the receiving end, i.e. inside the reducer, there is no difference between casting to interface or to class both are just compile time operations.
I often do it like this -> http://stackoverflow.com/questions/35482241/how-to-type-redux-actions-and-redux-reducers-in-typescript/41442542#41442542
I have an Action
interface
export interface Action<T, P> {
readonly type: T;
readonly payload?: P;
}
I have a createAction
function:
export function createAction<T extends string, P>(type: T, payload: P): Action<T, P> {
return { type, payload };
}
I have an action type constant:
const IncreaseBusyCountActionType = "IncreaseBusyCount";
And I have an interface for the action (check out the cool use of typeof
):
type IncreaseBusyCountAction = Action<typeof IncreaseBusyCountActionType, void>;
I have an action creator function:
function createIncreaseBusyCountAction(): IncreaseBusyCountAction {
return createAction(IncreaseBusyCountActionType, null);
}
Now my reducer looks something like this:
type Actions = IncreaseBusyCountAction | DecreaseBusyCountAction;
function busyCount(state: number = 0, action: Actions) {
switch (action.type) {
case IncreaseBusyCountActionType: return reduceIncreaseBusyCountAction(state, action);
case DecreaseBusyCountActionType: return reduceDecreaseBusyCountAction(state, action);
default: return state;
}
}
And I have a reducer function per action:
function reduceIncreaseBusyCountAction(state: number, action: IncreaseBusyCountAction): number {
return state + 1;
}