Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for typed Actions #355

Closed
razjel opened this issue Jul 29, 2015 · 14 comments
Closed

Support for typed Actions #355

razjel opened this issue Jul 29, 2015 · 14 comments
Labels

Comments

@razjel
Copy link

razjel commented Jul 29, 2015

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?

@gaearon
Copy link
Contributor

gaearon commented Jul 29, 2015

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.

@cesarandreu
Copy link
Contributor

Related: flux standard action

@razjel
Copy link
Author

razjel commented Jul 30, 2015

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?

@razjel
Copy link
Author

razjel commented Jul 30, 2015

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;
}

@gaearon
Copy link
Contributor

gaearon commented Jul 30, 2015

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.)

@nickknw
Copy link

nickknw commented Oct 14, 2015

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?

@gaearon
Copy link
Contributor

gaearon commented Oct 14, 2015

Can you try using discriminated unions instead of classes?
Using class and instanceof seems like the wrong way to achieve type safety here.

@nickknw
Copy link

nickknw commented Oct 14, 2015

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.

@nickknw
Copy link

nickknw commented Oct 14, 2015

(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.

@gaearon
Copy link
Contributor

gaearon commented Nov 17, 2015

See a workaround for TypeScript 1.6: #992 (comment)

@aikoven
Copy link
Collaborator

aikoven commented Dec 22, 2015

There is another approach for action typing: #992 (comment)

@techird
Copy link

techird commented Jan 20, 2016

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!

@repinvv
Copy link

repinvv commented Mar 4, 2016

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.

@elmerbulthuis
Copy link

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;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants