Lenses in functional programming, like their namesakes, are tools that allow you to manipulate focus. What this means in the FP context is that lenses let you manipulate a wider nested data structure by focusing on one of its constituent parts. One of the largest benefits of this approach is that lenses let you immutably update a given object without devolving into a spread operator mess. An example, using Lenses:

type Person = { age: number; height_in_inches: number };
const ageLens = Lens.from<Person>().prop("age");
let p1: Person = { age: 35, height_in_inches: 71 };
expect(ageLens.get(p1)).to.equal(35);
let p1Aged = ageLens.set(p1, 36);
expect(p1Aged.age).to.equal(p1.age + 1);

This might seem fairly trivial but luckily, like physical lenses, these lenses can be composed (think of a telescope as a composition of lenses):

type Address = { street_no: string, street_1: string, street_2: string | null, city: string, state: string, zip: string };
type Person = { age: number; address: Address };

const zipLens = Lens.from<Address>().prop("zip");
const addrLens = Lens.from<Person>().prop("address");
const zipInAddrLens = addrLens.comp(zipLens);

let p1: Person = {age: 35, address: {street_no: "1", street_1: "main st", street_2: null, city: "Sometown", state: "IL", zip: "55555"}};
expect(zipInAddrLens.get(p1)).to.equal("55555");
let p1WithUpdatedZip = zipInAddrLens.set(p1, "44444")
expect(p1.address.zip).to.equal("55555");
expect(p1WithUpdatedZip.address.zip).to.equal("44444");

You can also use a related construct called a prism for cases where an attribute could be undefined, like a given array element - luckily the lense library we’re using provides Arrays.index to handle this common case.

type Person = { age: number; favorite_numbers: number[] };
const numbersLens = Lens.from<Person>().prop("favorite_numbers");
const prismForNumberIndex = (index: number) => {
  return Arrays.index<number>(index);
}
const firstNumberInPerson = Prism.comp(numbersLens, prismForNumberIndex(0));
let p1: Person = { age: 35, favorite_numbers: [3, 1, 45] };
let firstNum: number | undefined = firstNumberInPerson.get(p1);
expect(firstNum).to.equal(3);
let p2 = firstNumberInPerson.set(p1, 22);
expect(p2.favorite_numbers[0]).to.equal(22);

This technique gives us a great way to refactor the mutation-heavy code in our implementation into something with more immutable data. Check out that version here, and for more reading on lenses check out: