Back to Engineering Blog

Enhancing TypeScript at LiveRamp

  • 3 min read

At LiveRamp, we’ve decided to adopt TypeScript for a lot of our backend services, a move motivated by the amazing benefits a type system provides. So far we are very happy with this choice. However, there were a few things we found repetitive, and so we decided to release a couple of packages to improve common workflows.

To build a helpful abstraction, I looked towards functional programming patterns I had used in the past. However, given that not everyone is familiar with functional programming, it was critical to create something more like what the team works with today. This means not going straight to functional purity, but setting out on the path towards it together. 

Typed Mutations

Source Code

A surprising amount of engineering is really just translating between data structures. However, the translation often requires frequent type casts and repetitive code blocks. To reduce the risk of human error, and increase the fluency, we decided to build an abstraction, allowing for “map” and “flatMap” style interactions that play nicely with the type system. 

While we were at it, we decided to implement it using lazy evaluation, which means the operations you do using map and flatMap do not execute until the resulting object is needed.

The resulting package lends itself well to in-place mutation without intermediate variables. This empowers you to express business logic fluidly by abstracting away the boilerplate. Here’s a link to a workbook where we’ll transform an object into a differently shaped object! 

An example using pluck() from KeyValues
An example using pluck() from KeyValues

 

Higher Order Promise

Source Code

Another very common task is aggregating multiple asynchronous results. For example, calling multiple APIs in order to construct a unified response. TypeScript and JavaScript have already drastically improved this area, especially with the introduction of async/await. However, one finds themselves trading off readability for performance as the syntax for parallelizing calls is rather delicate. 

We wrote Higher Order Promise to make dealing with multiple asynchronous requests a little easier to read, and a lot more robust. It exposes a simple abstraction allowing you to name your asynchronous tasks using simple Javascript objects, as well as letting you chain sequential calls using `then()` – and even better, the type information is kept up to date. 

Nothing talks quite like an example, so here’s an interactive RunKit notebook.

Example of chaining multiple promises
Example of chaining multiple promises

 

Summary and Next Steps

These two packages cut down on boilerplate, increasing developer satisfaction and reducing the likelihood of human error. They also maintain the types of the data involved, enabling a very tight feedback loop while the code is under iteration. Finally, their expressiveness enables code to track closely to the actual business logic, making it simple to understand its behavior in aggregate, rather than in isolation.

An interesting area to explore next is modifying mutations to work better with asynchronous data. Essentially, expressing these mutations and objects as Streams, rather than fully formed objects. Another area to explore is intelligent operation sequencing, e.g. moving filter steps earlier in the mutation stream to save unnecessary translation work.

Either way, we’re excited to share the journey together – please feel free to submit pull requests, file issues, or simply keep an eye on us on GitHub!