Composable, Immutable property access with Lenses in Typescript

Wow, that title has lots of potentially scary words. Wow you’re still here? Ok, here we go :)

A Lens is a way to set and get property values on an object. In Typescript we can define what that means with a short, if somewhat initially ambiguous, interface.

So I make no apologies for my love of generics. The first time I worked in a language that had them I knew I’d found something I could really enjoy. The problem is that without an example it’s probably not entirely clear what we’re doing with the interface. Let’s look at an example to help motivate why we’ve defined such an abstract interface.

Ok, so I think this starts to let us see what the Lens interface is all about. The first type argument is the type that the Lens will operate on, and the second is the type of the property the Lens targets. In this case the first property of the Name interface is a string, so a Lens targeting that property on that type will be Lens<Name, string>.

Immutability

The implementation for get simply accesses the property. The set implementation uses the spread operator ...a and then provides the new first value so that we return a new object rather than modifying the incoming object. If you’ve used Object.assign this is the same thing. Creating the new object is a way of ensuring immutability within our code.

Let’s look at another example..

So in this example we’ve used our previous Name interface as a property of a Person interface. As you might expect, if we want to focus on the name property with a Lens we’ll need to have a Lens<Person, Name>. The implementation looks very similar to the previous implementation with property and interface names changed so that our types all match.

Composability

Now that we have a Lens<Person, Name> and a Lens<Name, string> wouldn’t it be nice if we could combine the two lenses together to get a Lens<Person, string> ? That’s what composability is all about. The best part about it in this case is that composing two lenses has a property known as Closure. Basically that means we’re taking two values of type A to get another value of type A.

If you ever hear someone say something like “Integers are closed over addition” this is the thing that they’re talking about. Adding any two integers together always results in another integer. And just like integers with addition, any time we compose two lenses together it will result in another lens!

Any time we can get the Closure property into our code we immediately reduce the complexity of our code because combining things results in another thing of the same type. We could combine a thousand Lenses together and we’d still just have a Lens!

Let’s look at how we’d implement a function to combine two Lens interfaces.

So as before, the get implementation is pretty easy. We use the first Lens to get the first level and the second Lens to get the second level.

That set implementation looks a bit more complicated though… Don’t get hung up on the implementation and miss the benefit of what we get from being able to compose lenses though!

While this function is a bit more complex, it works for any two Lenses and so it only ever has to be written once, ever.

We need to get to the nested property with our x.get(A) so that we have a value our second Lens can target. Our second Lens is a Lens<B, C> which means it focuses on C properties of B objects and x.get(a) returns a B.

Once we have the B (the nested type) we can set it’s focused property to return a new B. And once we have an updated B we can set its property on our A (parent) type.

Putting it all together

Whew! ok, let’s once again see what we can do by leveraging our compose function!

Sweet! We can take an object of our Person type and immutably update the nested first property inside of the name property. We’ve composed two simple lenses into a new lens that navigates a nested object structure and preserves immutability.

My first impression of Lenses was that they were more work than they were worth because I could easily call me.name.first to get the value. While that may be the case on the get side of things, the set gets uglier and more complex with each nested level. Luckily by using simple Lenses composed together, our complexity remains static regardless of how many levels of nested properties we’re focusing into.

Like what you read? Give Reid Evans a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.