Designing a lightweight undo history with TypeScript

Maurice de Laat
7 min readJan 22, 2021

by Maurice de Laat

Designing an extensible undo/redo history using TypeScript and the Command Pattern.

In desktop applications, the ability to undo or redo a user’s latest action has already become ubiquitous many years ago. So it’s not surprising that, with web applications replacing their desktop counterparts more and more, users are just expecting it from modern web applications as well. The good news is that implementing an undo history doesn’t have to be hard. However, implementing it right from the beginning can save you a lot of headache later on.

When I started the development of JitBlox, I also decided that I wanted to offer undo-functionality right from the start. JitBlox is built with Angular, so a quick search for angular undo history already gave lots of useful links. However, most of the solutions I found were coupled to libraries that solved a lot more than just undo/redo. While that might be a good thing if one of these libraries fit within your architecture, I wanted to keep my implementation of such a key feature to be as much as possible framework-independent.

In this article, I will go through my design decisions while implementing my own, lightweight, undo/redo history in pure TypeScript. You can find a working implementation in a simple to-do manager app which can be found in this Git repository. Even though the demo is built with Angular, the core undo/redo functionality is framework independent (with a bit of RxJS on top of it to make things easier).

The application domain consists of just a single entity, which is named (as you might have already guessed) a TodoItem.

The app allows a user to add/remove to-do items and to update individual properties of a to-do item. It also demonstrates two actions that are less generic: one for stepping a numeric property (priority) with a specified amount, and one for toggling a boolean value (done).

Design goals

Let’s start by setting some design goals to keep in mind during the process.

  • Minimize dependencies: As already pointed out, I set myself a goal to build something independent from any state management library like Redux or it’s Angular counterpart NgRx. The Redux principle can be very valuable to your application (e.g. forcing immutable state, caching), but this really depends on the type of application you are building.
  • Lightweight: No snapshots of the application state. Some undo/redo solutions work by capturing the application state at each action (see the Memento pattern), meaning that objects need to be deep-cloned. While this may be ok for smaller applications, it might become inefficient when dealing with lots of data or a long undo-history.
  • Benefit from TypeScript: design a strongly-typed API that is easy to understand and prevents callers from making mistakes.
  • Extensible: Most undo/redo actions will apply to basic CRUD actions: adding new items to (or removing from) an array and updating properties of an object. But the solution shouldn’t be restricted to that and allow for implementation of custom, domain-specific actions.

Choosing a design pattern

It doesn’t take a lot of research to find that for implementing undo and redo, there are basically two design patterns to choose from:

  • Memento pattern: A memento represents something you want to hold onto. For undo/redo, this means that an application must be able to revert to the state in which it existed before a user executed an operation.
  • Command pattern: With the command pattern, you represent each operation as a unique object. As the user executes operations, corresponding command objects are stored in a history stack. As long as each command can support being “undone,” undo and redo is a simple matter of tracking back and forth through this stack (or, typically a “past” and a “future” stack).

Both patterns have their own advantages and disadvantages. Here’s a quick comparison:

Command design

Let’s start by defining the core of our design (without reinventing the wheel): the Command interface. This interface needs to be implemented by any class that performs an undoable action of any kind.

Also, I defined a CommandResult interface so that we know if the command got executed successfully and we know if/how we can store it in the history or communicate the result back to other parts of the application.

The core of our design consists of the following types working together.

  • UndoHistory: Stores executed commands in an undo- or redo-stack.
  • CommandHandler: The CommandHandler is responsible for executing commands, managing theUndoHistory and executing undo/redo commands.

Implementing the first command

Let’s implement the first command: one that updates a single property on a target object.

Note that this command uses keyof(TTarget) to make sure that the property is really a property of the target object, preventing callers from providing non-existent property names.

Executing a command

Now we have our first command (more commands will follow below), executing it looks as follows:

While this works, there are some points for improvement:

  • Callers must instantiate a concrete implementation of UpdatePropertyCommand. Wouldn't it be better to add some abstraction around the implementation details?
  • Callers might not even know that the UpdatePropertyCommand exists in the first place.

So what about refactoring the UpdatePropertyCommand a little bit and separate the command information from the actual implementation?

And update our concrete implementation as follows:

We have now opened up a way to just pass the command information to the command handler, and leave the command instantiation inside.

But how do we let the command handler know that it needs to deal with an “update property” command and not another command that happens to use similar command data? We could make an execute... function for each command, but that would make the solution less maintainable. A better option would be to assign a key to each command and let TypeScript ensure that each key is unique.

Implementing a command map

You may have seen the concept of event maps in TypeScript. An event map is, for example, used for mapping DOM event names to their corresponding event arguments. We can use the same mechanism for relating command keys to command data:

Let’s update the signature of commandHandler.execute(...) to use this command map:

Now, a caller can only pass a value for key that is part of the command map, with command data that corresponds to the key.

Separate concerns

Our command execution interface already got easier to use: callers are just talking to the CommandHandler, which takes care of creating the right command instance, executing it and adding it to the undo history. However, this approach isn't very flexible:

  • Whenever a new command is implemented, we need to update the internals of our CommandHandler implementation to make it work.
  • There is no way for callers to implement their own commands.

This brings me to the last update to our design, which is to move command instantiation to a factory method and revert the commandHandler.execute function so that it accepts any object that implements the Command interface.

Now, executing a command looks as follows:

This allows the CommandHandler to accept any object that implements the Command interface, and we still have a strongly-typed factory method for creating common commands. Also, the factory/command map duo can be easily extended to create other (probably more domain-specific) commands:

Implementing more generic commands

Until now, I’ve only used a generic update-property command as an example. Even for basic CRUD-applications we obviously need more than that. What about, for example, a command that updates multiple properties at the same time? Luckily they only need to be implemented once, so I've already created most of them in the demo project.

Here are some of them with their data interfaces:

UpdatePropertiesCommand

A command to update multiple properties of a target object.

As you can see, the UpdatePropertiesCommand makes use of TypeScript’s built-in Partial<T> type (a type with all properties of Type set to optional). Example usage with a TodoItem:

AddToChildCollectionCommand

Adds an element to a target object’s child collection. The propertyName can only be set to a property that is actually an array of a specified type.

Likewise, the example project also contains commands for removing elements from a collection (either by reference or by index).

StepNumberCommand

Increments a numeric property with a specified amount. The example project also uses this command is also used to increment (or decrement) a the priority of a TodoItem by steps of 3 (you manager will love this -:). This is a good example of a command that can be repeated infinitely.

Responding to undo/redo actions

At some point, you will need to synchronize your data with a backend. How and when to do this depends on your state management architecture, which is outside the scope of this article. The demo application demonstrates one way to plug into the undo/redo history: by making a wrapper around the CommandHandler that makes all actions observable (using RxJS in this case).

Then, a higher-level component or service can then subscribe to this as follows:

See it all in action

You can see it all in action by cloning the demo repository and runing npm install followed by ng serve -o. I'm curious to know what you think (are there similar solutions out there that I've missed? are there any points for improvement?). Feel free to drop a comment.

Originally published at https://www.jitblox.com on January 22, 2021.

--

--

Maurice de Laat

Freelance software designer | Creator of JitBlox online prototyping environment and Yellicode, an open source code generation platform based on TypeScript/Node.