Redux Actions for TypeScript

Last time we saw how React views are functions which take the current application state, and return a representation of the HTML. But how best to store the state?

Redux is a library and set of design contracts which together allow “time travel” and hot code reloading. In Redux the state is stored in a single JavaScript object, which somewhere deep inside may have an appointment collection like this:

    {
        appointments: {
            2553: {
                id: 2553,
                nature: Nature.Emergency,
                date: new Date(2015, 9, 15)
            },
            // ...more appointments
        }
    }

Changes to the store are also defined by JavaScript objects called actions, the action to remove an appointment from the store might look like this:

    {
        type: "RemoveAppointment",
        appointmentId: 2553
    }

Redux encourages making the actions and the store JSON-serialisable so that the application state can be re-wound and the actions re-played in the Redux dev tools, even as bugs are fixed and new features are added to the application during development.

Defining Action Classes

But this poses a problem: since the action objects can be serialised through JSON, their runtime type will always be plain JavaScript objects. How can we distinguish them using TypeScript?

To define the problem, lets say we have two actions:

    var removeAppointment = {
        type: "RemoveAppointment",
        appointmentId: 2553
    };

    var rescheduleAppointment = {
        type: "RescheduleAppointment",
        appointmentId: 2553,
        date: new Date(2015, 10, 15)
    };

In TypeScript we would like to verify that:

  1. No code should ever try to read the non-existent “date” property from a RemoveAppointment action.
  2. If a function expects a RemoveAppointment action, it should never accidentally be given a RescheduleAppointment action.

First lets define the two actions:

    abstract class Action {
        type: string;
    }

    class RemoveAppointment extends Action {
        constructor(public appointmentId: number) {
            super();
        }
    }
    
    class RescheduleAppointment extends Action {
        constructor(public appointmentId: number, public date: Date) {
            super();
        }
    }

This produces a compile-time error when accessing the date:

    var action = new RemoveAppointment(2553);
    action.date; // Compile error!

Infer Class from action.type

When an action’s type needs to be determined at runtime, a TypeScript type guard can be used:

    function handleAction(action: Action) {
        if (isType(action, RescheduleAppointment)) {
            action.date; // OK!
        }
    }

To implement the isType function, the type name needs to be accessible through the RemoveAppointment class as well as through the instance of the action.

A decorator can sets the type name on the action prototype, but unfortunately the name of a class isn’t provided to class decorators in TypeScript 1.6, so the name needs to be passed into the decorator too:

    interface ActionClass<T extends Action> {
        prototype: T;
    }
    
    function typeName(name: string) {
        return function<T extends Action>(actionClass: ActionClass<T>) {
            actionClass.prototype.type = name;
        }
    }

    @typeName("RemoveAppointment")
    class RemoveAppointment extends Action {
        constructor(public appointmentId: number) {
            super();
        }
    }

The type guard can then check that the type property on the action instance matches the type property on the action prototype:

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

When isType returns true, TypeScript will infer that the argument “action” is of type T.

Actions need to include the type property when serialised to JSON, but JSON.stringify ignores properties on prototypes, so the Action base class needs to copy the type property from the prototype to the instance of the action:

    abstract class Action {
        type: string;
        constructor() {
            this.type = this.type;
        }
    }

Distinguish Classes by Name

This solves the first challenge, reading properties from JSON deserialised actions can now be type-checked, but there is a problem: TypeScript uses a structural type system, so since the RescheduleAppointment action has all of the properties that the RemoveAppointment has, it can be used in its place:

    function doRemove(action: RemoveAppointment) {
        // ...
    }
    
    var action = new RescheduleAppointment(2553, new Date(2015, 10, 15));
    doRemove(action); // Oops!

To solve the second challenge, we need to introduce some nominal typing: the actions need to be distinguished based on their names, not just by their structure. This can be done by branding the class with a private property, since from the language handbook:

When an instance of a class is checked for compatibility, if it contains a private member, the target type must also contain a private member that originated from the same class.

Defining the special private property as “void” will to ensure that it is never set in type-checked code, but since void properties are unusual, we can hide it behind a type alias to make the usage more descriptive:

    type NominalType = void;
    
    @typeName("RemoveAppointment")
    class RemoveAppointment extends Action {
        private _brand: NominalType;
        constructor(public appointmentId: number) {
            super();
        }
    }
    
    var action = new RescheduleAppointment(2553, new Date(2015, 10, 15));
    doRemove(action); // Compile error!

That’s it! Fully type-checked actions which can be round-tripped as plain JSON objects.

Click to see the full code example.