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 (Typescript) #992

Closed
nickknw opened this issue Nov 2, 2015 · 71 comments
Closed

Support for typed actions (Typescript) #992

nickknw opened this issue Nov 2, 2015 · 71 comments

Comments

@nickknw
Copy link

@nickknw nickknw commented Nov 2, 2015

Related to these two issues:

Hi Dan,

The company I am with is using Redux as well as Typescript, and we would like to add some additional compile-time guarantees to our actions. Currently this isn't possible because of the restriction that actions must be plain objects. You said that this is because 'record/replay and associated features won't work with an action class' and I get that those are valuable features and don't want to lose them. But I had code using Typescript classes that (when the redux devtools were enabled) could still record, cancel and replay actions just fine (using the code I posted in issue 355).

When I remove the devtools I just get errors from that check that actions are plain objects and execution stop there. To me it seems like it is too strict, checking more than is really required. I would much rather have a check that requires that the actions are serializable. I know performance is a concern - IMO it shouldn't have to guarantee/prove they are serializable, just check that the action objects themselves say that they are, and then trust them.
(Not sure why the check doesn't also fail when devtools are enabled - maybe you can shed light on that).

I would really like to be able to leverage the advantages of both Typescript and Redux. Hope you can help me (and others using both of these tools) arrive at a solution.

I also want to reiterate that (as much as I wish it were otherwise!) Typescript does not have discriminated unions, so solutions based on that approach will not work.

Thank you!

@gaearon
Copy link
Contributor

@gaearon gaearon commented Nov 2, 2015

Does TypeScript let you type-check shapes of plain objects instead of defining classes?
My only issue is with using instances of classes for actions.

Wouldn't TypeScript interfaces be enough to strongly type plain object actions?

@nickknw
Copy link
Author

@nickknw nickknw commented Nov 2, 2015

Almost - the problem with those is that they only exist at compile-time. So the reducer will have no way to tell which incoming action is of which interface. ...Unless I keep the 'type' property on each action and then manually cast them after checking that property. And at that point I'm not getting any extra type-safety over just treating them as plain objects.

@alitaheri
Copy link

@alitaheri alitaheri commented Nov 2, 2015

@nickknw Well there can be no way to strong type those actions! I mean it wouldn't be possible even if it was C#! It's just how the architecture is, it doesn't have anything to do with redux. I my self use redux with typescript too. And best I could come up with was having interfaces with the same name as my actionTypes, keep the interfaces and constants in the same place, enforce my actionCreators to respect the typings and cast inside the reducers. It's not so hard to maintain and it gives me the type safety I need with clear intentions why the interfaces exist. I would suggest you to consider this approach, it worked for me 😁 and my project is kinda huge :D

@gaearon
Copy link
Contributor

@gaearon gaearon commented Nov 2, 2015

You said that this is because 'record/replay and associated features won't work with an action class' and I get that those are valuable features and don't want to lose them. But I had code using Typescript classes that (when the redux devtools were enabled) could still record, cancel and replay actions just fine (using the code I posted in issue 355).

Yes but then if you use persistState() enhancer, serialize them, and later restore your state and deserialize them, they will no longer work because your code is relying on instanceof checks.

The “plain objects” isn't really the point. What we want to enforce is that you don't rely on actions being instances of specific classes in your reducers, because this will never be the case after (de)serialization.

@gaearon
Copy link
Contributor

@gaearon gaearon commented Nov 2, 2015

In other words, this is an anti-pattern:

function reducer(state, action) {
  if (action instanceof SomeAction) {
    return ...
  } else {
    return ...
  }
}

If we make it possible for TypeScript users, I know JavaScript users will be tempted and will abuse this because many people coming from traditional OO languages are used to writing code this way. Then they will lose benefits of record/replay in Redux.

One of the points of Redux is to enforce some restrictions that we find important, and that, if not enforced, don't allow for rich tooling later on. If we let people use classes for actions, they will do so, and implementing something like redux-recorder will be impossible because there's no guarantee people use plain object actions. Especially if boilerplate projects suddenly decide to adopt classes because “wow, it’s allowed now, it must be a great idea”.

@nickknw
Copy link
Author

@nickknw nickknw commented Nov 2, 2015

What we want to enforce is that you don't rely on actions being instances of specific classes in your reducers, because this will never be the case after (de)serialization.

I think this is where you lost me. I don't follow why serializing and deserializing an object necessarily means that it will lose its class-related information.

@gaearon
Copy link
Contributor

@gaearon gaearon commented Nov 2, 2015

let actions = [new SomeAction(), new OtherAction()];
let json = JSON.stringify(actions);
let revivedActions = JSON.parse(json);
// reducers no longer understand revivedActions because type info is stripped.
@gaearon
Copy link
Contributor

@gaearon gaearon commented Nov 2, 2015

Another reason why I don't want to allow people to create classes for actions is because this will encourage folks to

a) Create class hierarchies
b) Put logic there, e.g. “patch” method from #437 (comment)

Both are contrary to how I intend Redux to be used. However React showed that if you give people ES6 classes, they will use inheritance even when there are much better alternatives, just because it's so entrenched in software development. It's hard to let go of old habits unless you're forced to.

It's sad that TypeScript forces you to turn things you want to strongly type into classes, but I believe this to be a fault of TypeScript, not ours. As described in #992 (comment), I find casting to be the best solution we can offer. You can also publish redux-typescript-store project with createStore() that doesn't have this restriction, but I'm still not compelled to remove it.

@gaearon
Copy link
Contributor

@gaearon gaearon commented Nov 2, 2015

Finally: for the record, I'm not hatin' on classes.
That's not my point.

My only point is that actions should be passive descriptions of what happened. If we allow them to be class instances, people will build weird abstractions on top of actions with classes, and suffer as a result.

Do not want.

@nickknw
Copy link
Author

@nickknw nickknw commented Nov 2, 2015

Ahh okay, thank you for the example. I didn't realize that JSON.stringify discards prototype information. It doing that makes sense to me in retrospect, given the limitations of what JSON can store.

One of the points of Redux is to enforce some restrictions that we find important, and that, if not enforced, don't allow for rich tooling later on.

Yes, I get that, am on board with it, and don't want to compromise on the guarantees the tools can count on. I don't want to create class hierarchies or attach logic to classes, just want to be able to tell them apart at compile-time.

It's sad that TypeScript forces you to turn things you want to strongly type into classes, but I believe this to be a fault of TypeScript, not ours.

Interfaces work well enough for what they were designed for, but I agree it is unfortunate for this use case that they don't keep any type information around at runtime (or allow for a runtime check 'does this conform to this interface' in another way).

My only point is that actions should be passive descriptions of what happened.

100% agree with you here

If we allow them to be class instances, people will build weird abstractions on top of actions with classes, and suffer as a result.

Ugh, I suppose that's true too.


I still think there might be room for a middle ground here, with a slightly more flexible way of serializing actions. There's the 'reviver' and 'replacer' parameters for parse and stringify - maybe there is a way to create a serialization function that can preserve enough information for me to pass some basic type information through but also know enough about the structure to warn/error about the scenarios you want to prevent.

Would rather not go down the route of manually casting actions - I might if nothing else works out but at that point the types aren't adding a lot of value for the effort.

@nickknw
Copy link
Author

@nickknw nickknw commented Nov 2, 2015

And yes it would be nice if Typescript had more flexible typing options so that I didn't have to use classes but that isn't something I can realistically affect or count on changing any time soon.

@gaearon
Copy link
Contributor

@gaearon gaearon commented Nov 2, 2015

maybe there is a way to create a serialization function that can preserve enough information for me to pass some basic type information through but also know enough about the structure to warn/error about the scenarios you want to prevent.

This would involve user implementing functions that map type strings to classes which is pretty much manual casting all over again. And we don't want users to implement this sort of stuff to get record/replay working. This is another point of Redux: you get hot reloading, record/replay for free, without implementing serialize(), hydrate(), etc.

@nickknw
Copy link
Author

@nickknw nickknw commented Nov 4, 2015

This would involve user implementing functions that map type strings to classes which is pretty much manual casting all over again.

Ideally it would just involve implementing a pair of functions that do that, and then all the action classes would be implemented in a way that those functions could serialize them. So instead of possibly introducing human error once for each different action handled, it would just be once.

And we don't want users to implement this sort of stuff to get record/replay working.

Not what I was proposing - this could be something that is opt-in. If you don't need to do it everything works as normal. I understand you might not be enthusiastic about opening that can of worms. I don't mind digging into this at some point and see if I can find some way to preserve the type information I want without opening the door wide open for everything else.

This is another point of Redux: you get hot reloading, record/replay for free, without implementing serialize(), hydrate(), etc.

This is a great benefit of Redux and I wouldn't want to change that. But I would also love to get some compile-time guarantees for actions while using Redux.

@gaearon
Copy link
Contributor

@gaearon gaearon commented Nov 4, 2015

I don't want to change it in core for reasons above. As I said previously, please feel free to create redux-typescript package with TS-friendly version of createStore and (maybe?) other Redux functions. We don't intend to have breaking changes in Redux at this point so you shouldn't worry about keeping up with the API. And as long as the API matches, such custom store will work just fine. (And its implementation is tiny, check out createStore source).

@Matthias247
Copy link

@Matthias247 Matthias247 commented Nov 13, 2015

I think you can solve the issue by using TypeScript 1.6 user-defined type guards (see http://blogs.msdn.com/b/typescript/archive/2015/09/16/announcing-typescript-1-6.aspx).

Define an interface for the base action and one for each other action. Additonally for each Action create a isXYZAction(a: Action) function which checks the type based on the value of the type field (which is defined in the base action interface.

Use these functions in the reducer to safely access fields of the specific actions after the check:

interface Action {
  type: string
}

interface AddTodoAction extends Action {
  text: string
}

function isAddTodoAction(action: Action): action is AddTodoAction {
  return action.type == ADD_TODO;
}

function reducer(state: any, action: Action) {
  if (isAddTodoAction(action)) {
    // typesafe access to action.text is possible here
  } else {
    return ...
  }
}
@frankwallis
Copy link

@frankwallis frankwallis commented Nov 15, 2015

@Matthias247 that's exactly what I need, thanks!

@nickknw
Copy link
Author

@nickknw nickknw commented Nov 17, 2015

@Matthias247 That's fantastic, thank you for pointing that out! Did not know about that feature, that is exactly what I need :)

@aikoven
Copy link
Collaborator

@aikoven aikoven commented Dec 22, 2015

I'm using FSA and I found it very convenient to have generic Action interface with type parameter corresponding to payload type:

interface Action<T>{
  type: string;
  payload: T;
  error?: boolean;
  meta?: any;
}

Then I export payload type alongside action type using same name:

export const ADD_TODO = 'ADD_TODO';
export type ADD_TODO = {text: string};

This way when you import ADD_TODO, you actually import both action type and payload type:

import {ADD_TODO} from './actions';
import {handleActions} from 'redux-actions';

const reducer = handleActions({
  [ADD_TODO]: (state, action:Action<ADD_TODO>) => {
    // enabled type checking for action.payload
    // ...
  }
});
@igorbt
Copy link

@igorbt igorbt commented Dec 30, 2015

I had the same problem, and found the solution here: http://www.bluewire-technologies.com/2015/redux-actions-for-typescript/. It's basically a generalization of solution proposed by @Matthias247, and it has a clear advantage: no need to define a is*Action() function for each action.

This solution seems to work, but it needs 2 additions for Redux. First you need a middleware for transforming actions to plain objects (redux does this checking and throws an error). This could be as simple as:

// middleware for transforming Typed Actions into plain actions
const typedToPlain = (store: any) => (next: any) => (action: any) =>  {
    next(Object.assign({}, action));
};

And if you use tslint, it will complain about unused _brand property, so the only thing you can do is to enclose it in tslint ignore comments:

/* tslint:disable */
private _brand: NominalType;
/* tslint:enable */

BTW, I didn't use NominalType as it seemed to add to the boilerplate, and just used directly void type.

@AlexGalays
Copy link

@AlexGalays AlexGalays commented Feb 9, 2016

For those interested in a 'flux' lib with native support for typescript, you may want to check: https://github.com/AlexGalays/fluxx#typescript-store

@sompylasar
Copy link

@sompylasar sompylasar commented Feb 9, 2016

@AlexGalays Good job so far! Didn't you think of instead forking Redux and adding TypeScript support to it, so the community gets all the features Redux and its friend modules provide, like DevTools, middleware, sagas etc?

UPD Oh, I see, this #992 (comment) answers my question. Then why fluxx and not just redux-typescript? And is it compatible with Redux-based tools?

UPD2 By looking more deeply, the only addition to Redux is the typings and tsconfig.json -- can't we get a repo with only these, and reuse and delegate maintenance of Redux to Redux devs, to avoid having all this callbacks and Dispatch in the middle of dispatch and Store.log implemented in the TypeScript-oriented library?

@AlexGalays
Copy link

@AlexGalays AlexGalays commented Feb 9, 2016

@sompylasar I asked myself that same question at some point: "Why not just use redux, it looks pretty close to what I want from a state container"; But 1) a bit of competition is always good (Speaking in general. fluxx obviously get obliterated by redux competition wise), 2) I wish to keep it simple and easy to learn for my coworkers. That's what I get with fluxx that I don't get with redux: Just one npm dependency for all my needs (yet it's even tinier than redux, you can check!), less boilerplate (the actions are self dispatching and it's very quick to write new ones) and the guarantee that my typescript definitions stay up to date (I don't believe in external definitions except for things like react where you can be fairly sure various people will keep it up to date)

On the other hand I have no need for many things redux and its various extensions provide.

By the way, my abyssa router is also getting typescript definitions soon and I've found working with the combination of abyssa/fluxx/immupdate to be very enjoyable.

@gaearon
Copy link
Contributor

@gaearon gaearon commented Feb 9, 2016

less boilerplate (the actions are self dispatching and it's very quick to write new ones)

FWIW, from http://redux.js.org/docs/recipes/ReducingBoilerplate.html:

Actions are plain objects describing what happened in the app, and serve as the sole way to describe an intention to mutate the data. It’s important that actions being objects you have to dispatch is not boilerplate, but one of the fundamental design choices of Redux.

There are frameworks claiming to be similar to Flux, but without a concept of action objects. In terms of being predictable, this is a step backwards from Flux or Redux. If there are no serializable plain object actions, it is impossible to record and replay user sessions, or to implement hot reloading with time travel. If you’d rather modify data directly, you don’t need Redux.

@gaearon
Copy link
Contributor

@gaearon gaearon commented Feb 9, 2016

One interesting competing project that is TypeScript friendly is https://github.com/ngrx/store
It is not fully Redux-compatible but people like using it with Angular.

@AlexGalays
Copy link

@AlexGalays AlexGalays commented Feb 9, 2016

@gaearon I know why you did it, I'm not saying "redux could have less boilerplate". I've just chosen a different set of tradeoffs :p

@Cooke
Copy link

@Cooke Cooke commented Apr 24, 2016

After playing with the great suggestions here (mainly @jonaskello's) I found two small drawbacks that I wanted to overcome:

  • It is a bit awkward to use action creators in the reducers. Feels strange with the camel-casing, no type highlighting and just having them there in general.
  • The need for a action creator in the first place. I have a mostly server driven application where many actions are instantiated on the server and the type definitions are generated.

Here is my suggestion:

export class ConnectAction {
    static type = "Connect";
    host: string;
}

// Variant1: less boilerplate to create
export const connect1 = common.createAction(ConnectAction)

// Variatn2: less boilerplate to call
export const connect2 = common.createActionFunc(ConnectAction, (host: string) => ({ host: host}))

Invoking action creators:

connect1({ host: 'localhost'}) 
connect2('localhost');

Reducer:

const reducer = (state: any = {}, action: Action) => {
    if (is(action, ConnectAction)) {
        console.log('We are currently connecting to: ' + action.host);
        return state;
    } else {
        return state;
    }
}

The common/generic stuff:

export interface ActionClass<TAction extends Action> {
    type: string;
    new (): TAction;
}

export interface Action {
    type?: string;
}

export function createAction<TAction extends Action>(actionClass: ActionClass<TAction>): (payload: TAction) => TAction {
    return (arg: TAction) => {
        arg.type = actionClass.type;
        return arg;
    };
}

export function createActionFunc<TAction extends Action, TFactory extends (...args: any[]) => TAction>(
    actionClass: ActionClass<TAction>,
    actionFactory: TFactory): TFactory {
    return <any>((...args: any[]): any => {
        var action = actionFactory(args);
        action.type = actionClass.type;
        return action;
    });
}

export function is<P extends Action>(action: Action, actionClass: ActionClass<P>): action is P {
    return action.type === actionClass.type;
}

The drawbacks with this solution are:

  • Unconventional usage of classes - they are never instantiated.
  • Have to make the type property i the Actions interface optional (this is super minor to be).
  • It is cumbersome to add optional properties to the actions since they are classes (have to add an additional interface).

Even though the new drawbacks this solution works better for my current use cases. Also I don't use the Flux Standard Action style but I bet one can make such a variant with my suggestion.

@gaearon
Copy link
Contributor

@gaearon gaearon commented Apr 24, 2016

Note that we have added some typing files in 3.5.x.
If those aren’t as helpful feel free to raise an issue to discuss how they can be improved.

@aikoven
Copy link
Collaborator

@aikoven aikoven commented May 23, 2016

@Cooke see #992 (comment). I don't see anything wrong with having action creators in reducers because they only encapsulate action type string and payload shape. Of course there's also action instantiation logic, but there's no difference with classes: they too contain its own type info along with construction logic.

@k4tan
Copy link

@k4tan k4tan commented Jun 23, 2016

@geon In regards to the "String vs. string" problem", how about:

export interface _ActionType<T>{}
export type ActionType<T> = string & _ActionType<T>

// important to use _ActionType here
export function isAction<T extends Action>(action: Action, type: _ActionType<T>): action is T;
export function isAction<T extends Action>(action: Action, type: string): action is T {
    return action.type === type
}

You can use this as before but it will be a regular string and not a String.

@mweels
Copy link

@mweels mweels commented Jun 28, 2016

This approach is working good...

  1. Get typed actions and payload to dispatch.
  2. Redux gets clean plain objects
  3. Reducers gets typed action types, and typed payloads.

First setup enums..

export enum ActionTypes { TYPE_SELECTED }

create your commands...

export class ActionSelectedCommand  {
  constructor(public actionitem: any) {
  }
}

Create your function to call when dispatching (notice I am creating a standard object that in a sense serializes my command object).

So every action will always have a type and a payload.

export function ActionSelected(actionItem): any {
  return {
    type: ActionTypes.TYPE_SELECTED,
    payload: JSON.stringify(new ActionSelectedCommand(actionItem))
  };
}

Then on your reducer...


  if (!state)
    state = new NewStore();

  let type: actions.ActionTypes = <actions.ActionTypes>action.type;  -- constant action type :)

  switch (type) {
    case actions.ActionTypes.TYPE_SELECTED: {  -- more constact action type
      var warmAndFuzzyPayLoad = <actions.ActionSelectedCommand>JSON.parse(action.payload)
      -- Then you deserialize back to your action command with all your parameters type set.
      return state;
    }
    default:
      return state;
  }

Then dispatch away...

store.dispatch(actions.ActionSelected("Tight Actions"));

So far this seems to be working well.

It forces to keep the action commands very simple because any recursion or self referencing will blow this up.

I like the idea of keeping the redux store type free. This allows the ability to have flexibility in and out of the bus.

Cheers

@frankwallis
Copy link

@frankwallis frankwallis commented Jun 28, 2016

I think that discriminated union types which are arriving in TypeScript 2.0 should be a good solution for this issue.

@huiwang
Copy link

@huiwang huiwang commented Aug 25, 2016

My dream action support would be something like elm or scala.js where reducer is powered by pattern matching. But it seems typescript would not have pattern matching in the near future :(

@vinga
Copy link

@vinga vinga commented Sep 17, 2016

Another version with plain objects in reducer, based mainly on @Cooke.
It allows to inline action payload fields and omit explicit Action definition.
ActionTypes here are used as generic action creators.

export interface Action {
    type?: string;
}
export class ActionType<T extends Action>{
    type: string;
    constructor(type: string) {
        this.type=type;
    }
    new(t: T):T {
        t.type=this.type;
        return t;
    }
}

export function isAction<T extends Action>(action: Action, type: ActionType<T>|string): action is T {
    if (typeof type === 'string') {
        return action.type==type;
    } else if (type instanceof ActionType) {
        return action.type === type.type;
    }
    return false;
}

// here come action creators
const FOO: ActionType<{foo: string, foo2: string}> = new ActionType<any>('FOO');
interface BarAction extends Action {
    bar: string;
}
const BAR: ActionType<BarAction> = new ActionType<BarAction>('BAR');

in reducer:

export default function reducer(state = [], action:Action) {
    if (isAction(action, FOO)) {
        action.foo // have access
        return state;
    } else if (isAction(action, BAR)) {
        action.bar;
        return state;
    } else
        return state;
}

create new action like this

FOO.new({foo: 'foo'}) // error, foo2 is required
FOO.new({foo: 'foo', foo2: 'foo'}) // OK

// dispatch it like this
dispatch(FOO.new({foo: 'foo', foo2: 'foo'}));

You have type safe payload and don't have to use designated task creator.
You still can create actions old way. Main drawback is that type is not required in Action interface.

@amadeogallardo
Copy link

@amadeogallardo amadeogallardo commented Sep 24, 2016

Here's a new version based on the proposals by @aikoven, @jonaskello and inspired on redux-actions.
The goal is to to address these specific scenarios:

  1. Type safety for state, actions and payload
  2. Allow sharing of type safe state and local variables for all handlers inside reducer functions
  3. Preserve standard action creator function syntax to allow for logic, thunks, etc.
  4. Reduce switch-case boilerplate in reducers

Here it goes, let me know if you see any major flaws!

[EDIT: Refactored and renamed the HandleActions function as per @alexfoxgill's comments

// FSA Compliant action
interface Action<P> {
    type: string,
    payload?: P,
    error?: boolean,
    meta?: any
}

interface Todo {
    text: string
}

type HandlerMap<T> = {
    [type: string]: { (action: Action<any>): T }
}

// Handle actions "inspired" by redux-actions
// Only pass the action parameter to handlers, allowing to share type-safe state in the reducer
export const DEFAULT = "DEFAULT";
    export function handleAction<T>(
    action: Action<any>,
    handlers: HandlerMap<T>,
    defaultState: T): T {
    // As improved by @alexfoxgill
    const handler = handlers[action.type] || handlers[DEFAULT];
    // Execute the function if exists passing the action and return its value.
    // Otherwise return the Default state. 
    return handler ? handler(action) : defaultState;
}

    // If no handlers matched the type, execute the DEFAULT handler 
    if (handlers[DEFAULT]) {
        return handlers[DEFAULT](action);
    }

    // If DEFAULT not declared, return the state from the parameter
    return defaultState;
}

export const ADD_TODO = "ADD_TODO";
// Declare the type for the payload
export type ADD_TODO = Action<string>
// Standard action creator pattern with type safe return value
export const addTodo = (text: string): ADD_TODO => {
    // Allows for some complex logic, async behavior, thunks, etc.

    return {
        type: ADD_TODO,
        payload: text,
        meta: {
            foo: "Bar"
        }
    }
}

export const DELETE_TODO = "DELETE_TODO";
export type DELETE_TODO = Action<number>
export const deleteTodo = (index: number): DELETE_TODO => {
    // Some complex logic, async behavior, thunks, etc.

    return {
        type: DELETE_TODO,
        payload: index,
    }
}

// Standard pattern for creating reducers
// Type safety for state and return value, shared for all handlers
export const todoReducer = function (
    state: Todo[] = [],
    action: Action<any>): Todo[] {
    // Local variables can be declared and shared by all handlers
    let foo = action.meta && action.meta.foo;

    // Type safety for return value of state
    return handleActions<Todo[]>(action, {
        // Less boilerplate by removing the 'return' and 'break' statements
        // Payload is a string 
        [ADD_TODO]: ({payload}: ADD_TODO) => [...state, { text: payload }],
        // Payload is a number
        [DELETE_TODO]: ({payload}: DELETE_TODO) =>
            [...state.slice(0, payload), ...state.slice(payload + 1)],
        // For more complex actions, expand function
        [DEFAULT]: ({payload}) => {
            // Can handle other operations that don't depend on type
            if (foo) {
                console.log(foo);
            };
            return state;
        }
    },
        // In case DEFAULT is not implemented, return the default state 
        state);
}

// Dispatch in the context of a connected component
addTodo("Foo");
deleteTodo(0);
@alexfoxgill
Copy link

@alexfoxgill alexfoxgill commented Sep 30, 2016

@Agalla81 I like this design. I'm confused about the handleActions implementation though - what's the loop for? Wouldn't the following work:

type HandlerMap<T> = {
    [type: string]: { (action: Action<any>): T }
}

export function handleAction<T>(action: Action<any>, handlers: HandlerMap<T>, defaultState: T): T {    
    const handler = handlers[action.type] || handlers[DEFAULT];
    return handler ? handler(action) : defaultState;
}

You can also define a helper function for this pattern like so:

type HandlerMapWithState<T> = {
    [type: string]: { (action: Action<any>): (state: T) => T }
}

export function createReducer<T>(defaultState: T, handlers: HandlerMapWithState<T>) {
    return (state: T = defaultState, action: Action<any>): T => {
        const handler = handlers[action.type] || handlers[DEFAULT];
        return handler ? handler(action)(state) : state;
    };
}

Which lets you create a reducer with this syntax:

const reducer = createReducer(0, {
    INCREMENT: (a: INCREMENT) => s => s + a.payload.incBy,
    DECREMENT: (a: DECREMENT) => s => s - a.payload.decBy
});
@amadeogallardo
Copy link

@amadeogallardo amadeogallardo commented Sep 30, 2016

@alexfoxgill You're correct, the loop was unnecessary. This has been updated based on your suggestion for the handleAction implementation, thanks for noticing.

I like the createReducer approach but it may collide with one of the items that I wanted to address:
"2. Allow sharing of type safe state and local variables for all handlers inside reducer functions".

  • Type Safe state is indeed shared with the "double" arrow function "=> s =>".
  • But how can shared local variables be defined when using createReducer?
    In some scenarios, I find it useful to define those if there's common logic that can be re-used by multiple action handlers inside the reducer.
@alexfoxgill
Copy link

@alexfoxgill alexfoxgill commented Oct 4, 2016

@Agalla81 I guess that's a fair point, though I've not encountered a time when I've needed that common logic within the reducer definition itself (normally it's in other functions)

@amadeogallardo
Copy link

@amadeogallardo amadeogallardo commented Oct 8, 2016

@alexfoxgill here's an example from Redux's documentation that shows such a case scenario, by declaring a "post" local variable that can be reused in different action handlers.
Source: http://redux.js.org/docs/recipes/reducers/UpdatingNormalizedData.html

// reducers/posts.js
function addComment(state, action) {
    const {payload} = action;
    const {postId, commentId} = payload;

    // Look up the correct post, to simplify the rest of the code
    const post = state[postId];

    return {
        ...state,
        // Update our Post object with a new "comments" array
        [postId] : {
             ...post,
             comments : post.comments.concat(commentId)             
        }
    };
}
@aikoven
Copy link
Collaborator

@aikoven aikoven commented Oct 11, 2016

Just published a package implementing my above approach:
https://github.com/aikoven/redux-typescript-actions

@awesson
Copy link

@awesson awesson commented Jan 22, 2017

I stumbled upon this thread when trying to use concrete classes for actions like @nickknw to enforce type safety and redux reasonably yelled at me for not using plain objects.

I realized that with with Typescript 2.0, @frankwallis' premonition has come true, so I figured I would update with an example for anyone else who ends up here.

/* define the waxing actions and create a type alias which can be any of them */
interface WaxOn
{
    type: "WaxOn";
    id: number;
    waxBrand: string;
}

function createWaxOnAction(id: number, waxBrand: string): WaxOn
{
    return { type: "WaxOn", id, waxBrand };
}

interface WaxOff
{
    type: "WaxOff";
    id: number;
    spongeBrand: string;
}

function createWaxOffAction(id: number, spongeBrand: string): WaxOff
{
    return { type: "WaxOff", id, spongeBrand };
}

type AnyWaxingAction = WaxOn | WaxOff;

/* define the dodging actions and create a type alias which can be any of them */
interface Bob
{
    type: "Bob";
    id: number;
    bobDirection: number[];
}

function createBobAction(id: number, bobDirection: number[]): Bob
{
    return { type: "Bob", id, bobDirection };
}

interface Weave
{
    type: "Weave";
    id: number;
    weaveDirection: number[];
}

function createWeaveAction(id: number, weaveDirection: number[]): Weave
{
    return { type: "Weave", id, weaveDirection };
}

type AnyDodgeAction = Bob| Weave;

type AnyActionAtAll = AnyWaxingAction | AnyDodgeAction;
// action must implement one of the Action interfaces or tsc will yell at you
const reducer = (state: State = {}, action: AnyActionAtAll) =>
{
    switch(action.type)
    {
        case "WaxOn":
            // Will only let you access id and waxBrand
        case "WaxOff":
            // Will only let you access id and spongeBrand
        case "Bob":
            // Will only let you access id and bobDirection
        case "Weave":
            // Will only let you access id and weaveDirection
        default:
            // Make sure there aren't any actions not being handled here
            // that are included in the union
            const _exhaustiveCheck: never = action;
            return state;
    }
}

If you accidentally name two actions with the same type string, typescript wont necessarily yell at you, but in the switch-case for that string, typescript will only let you access the members which both interfaces have in common.

@jonaskello
Copy link

@jonaskello jonaskello commented Jan 22, 2017

@awesson You might want to assert that action is of type never in the default case in the switch statement. This way you can be sure that all actions are handled and you don't forget to add more cases to the switch if you create new actions. See this page under the "Exhaustive Checks" section for more info.

@awesson
Copy link

@awesson awesson commented Jan 22, 2017

Thanks @jonaskello. That's a good point.

I updated my example, but really it's probably better to just look at the example you linked. (I don't know how I missed that.)

One note about the exhaustive check is that if you are just starting to write actions for a new module following this organization and you only have 1 action so far, the type alias wont be a union and so the exhaustive check will always fail.

@txomon
Copy link

@txomon txomon commented Jun 13, 2017

I have been looking at different solutions for full typing support (state and actions), and I have found https://gist.github.com/japsu/d33f2b210f41de0e24ae47cf8db6a5df + @awesson's solution to be a really good combination

@Cooke
Copy link

@Cooke Cooke commented Jun 13, 2017

After playing around with the different alternatives I settled with the following approach https://github.com/Cooke/redux-ts-simple

@aj0strow
Copy link

@aj0strow aj0strow commented Aug 25, 2017

You can use union types but you can also use a big typed action object.

// Before

{
    type: "UserLoaded",
    payload: {
        user: { id: 4 }
    }
}

// After

{
    type: "UserLoaded",
    userLoaded: {
        user: { id: 4 }
    }
}

In the first case, there are lots of different action objects so you need a union type.

interface UserLoaded {
    type: "UserLoaded",        // redundant
    payload: {
        user: User;
    };
}

type MyAction = UserLoaded | OtherAction

In the second case, there is just one action type with lots of payload keys.

interface UserLoaded {
    user: User;
}

interface MyAction {
    type: string;
    userLoaded?: UserLoaded;
    otherAction?: OtherAction;
}

You can reduce by matching the type by string.

function userById(state = {}, action: MyAction) {
    if (action.type == "UserLoaded") {
        // action.userLoaded.user
    }
}

You can also check presence of payload key.

function userById(state = {}, action: MyAction) {
    if (action.userLoaded) {
        // action.userLoaded.user
    }
}

If you use presence, you don't rely on type anymore for actual reducing so you don't need to worry if a someone used the same name in another part of your project. You can also nest action payloads relating to a specific widget or domain inside a parent action object that includes context.

enum Field {
    Title,
    Body,
}

interface TextChange {
    field: Field;
    newText: string;
}

interface Submit {

}

interface NewPostForm {
    pageId: string;
    textChange?: TextChange;
    submit?: Submit;
}

interface MyAction {
    type: string;
    newPostForm?: NewPostForm;
}

If you really love union-ing actions from sub modules you can still use interface inheritance.

interface UserLoaded {
    user: User;
}

interface UserAction {
    userLoaded?: UserLoaded;
}

interface MyAction extends UserAction {
    type: string;
}

So yeah, it plays well with JSON serialization, existing redux tooling, and it's a nice balance between encapsulating some parts while still allowing cross-cutting actions.

@aminpaks
Copy link

@aminpaks aminpaks commented Oct 13, 2017

I guess everyone has already mentioned most of the solutions. My two cents on this topic, I just wanna emphasize about @aikoven approach. Even though I found this topic just now, the approach of my lib is very close to his.

The only difference of my approach compare to @aikoven is we keep track of our actions by their ClassAction.is(..) / ClassAction.type instead of isType(..) in reducers or epics/side effects.

Here how I've done it, I tried to remove all the boilerplate as much as possible:

feature-x.actions.ts

import { defineAction, defineSymbolAction } from 'redux-typed-actions';
import { ItemX } from './feature-x.types';

// For this action we don't have any payload
export const FeatureXLoadAction = defineAction('[Feature X] Load');

// Let's have a payload, this action will carry a payload with an array of ItemX type
export const FeatureXLoadSuccessAction = defineAction<ItemX[]>('[Feature X] Load Success');

// Let's have a symbol action
export const FeatureXDummySymbolAction = defineSymbolAction<ItemX[]>('[Feature X] Dummy Started');

feature-x.component.ts

import { ItemX } from './feature-x.types';
import { FeatureXLoadAction, FeatureXLoadSuccessAction } from '../feature-x.actions';
...
store.dispatch(FeatureXLoadAction.get());

// or in epics or side effects
const payload: ItemX[] = [{ title: 'item 1' }, { title: 'item 2' }];
store.dispatch(FeatureXLoadSuccessAction.get(payload));
// And a more strict check for payload param
store.dispatch(FeatureXLoadSuccessAction.strictGet(payload));

feature-x.reducers.ts

import { PlainAction } from 'redux-typed-actions';
import { ItemX } from './feature-x.types';
import { FeatureXLoadAction, FeatureXLoadSuccessAction } from '../feature-x.actions';

export interface ItemXState {
  items: ItemX[];
  loading: boolean;
}

...
export function reducer(state: ItemXState = InitialState, action: PlainAction): ItemXState {
  if (FeatureXLoadAction.is(action)) { // Visually helps developer to keep track of actions
    // Within this branch our action variable has the right typings
    return {
      ...state,
      loading: true,
    }

  } else if (FeatureXLoadSuccessAction.is(action)) {
    return {
      ...state,
      loading: false,
      items: action.payload, // <- Here we are checking types strongly :)
    }

  } else {
    return state;
  }
}

feature-x.epics.ts (redux-observable related)

...
class epics {
   loadEpic(): Epic<PlainAction, AppState> {
    return (action$, _store) => action$
      .ofType(FeatureXLoadAction.type)
      .switchMap(() =>
        this.service.getRepositories()
          .map(repos => StoreService.transformToItem(repos))
          .delay(2000)
          .map(repos => FeatureXLoadSuccessAction.strictGet(repos))
          .catch((error) => Observable.of(FeatureXLoadFailedAction.strictGet(`Oops something went wrong:\n\r${error._body}`))));
}

Here a live example

@nasreddineskandrani
Copy link

@nasreddineskandrani nasreddineskandrani commented Oct 13, 2017

@jonaskello i liked your comment about symbols. You mention:
If you want, you could modify the actionCreator() function in his solution to generate unique strings automatically since you will never refer to the strings directly in the code.
if i am not wrong the devtools will display these strings? we still need to generate unique string that make sense related to the real action and not just a uid.

So i think what @aikoven propose to don't have this problem is not that bad.

export function actionCreator<P>(type: string): ActionCreator<P> {
 if (actionTypes[type])
    throw new Error('Duplicate action type: ${type}');
...
@aminpaks
Copy link

@aminpaks aminpaks commented Oct 13, 2017

@nasreddineskandrani Or a better solution would be to check the process.env.NODE_ENV and if it's development warn the developer and if it's production just generate a random hash and attach it to the end of action type value to make it unique. This way it will never fail in prod across a large codebase.

@nasreddineskandrani
Copy link

@nasreddineskandrani nasreddineskandrani commented Oct 13, 2017

@aminpaks i think devtool with real action name in prod is useful.
https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f
example: What if we want the end-users to help the debugging

@aminpaks
Copy link

@aminpaks aminpaks commented Oct 13, 2017

@nasreddineskandrani I never said we should remove the initial action type. I meant let's add a hash to the end so we'll have an unique action.

@nasreddineskandrani
Copy link

@nasreddineskandrani nasreddineskandrani commented Oct 13, 2017

one of the dev in this thread mention this:
.... I have a mostly server driven application where many actions are instantiated on the server and the type definitions are generated.
with a dynamic type strategy take in count that your lib needs the exact same hash in 2 differents servers for a specific action and not a random one.

@aminpaks
Copy link

@aminpaks aminpaks commented Nov 2, 2017

I recently added a React example using redux-typed-actions to demonstrate how this strategy can help to improve productivity and will remove all boilerplates.

Take a look here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Linked pull requests

Successfully merging a pull request may close this issue.

None yet