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
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.
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
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
Let’s look at how we’d implement a function to combine two
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.
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
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
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.