DEV Community

Ken Aguilar
Ken Aguilar

Posted on • Edited on

3 1

Typescript - Manipulating Deeply Nested Immutable Objects with Lenses

Javascript doesn't really have an immutable object but with typescript I can
prevent compilation if there's a rogue function that will try to mutate an object.

Let's say I have this object, and let's assume the values are always there
because if I start talking about the possibility of null then I have to talk
about prisms. So let's take it easy and stick with lenses for now.

const bankIdentity: BankIdentity = {
  account: {
    owner: {
      address: {
        data: {
          city: "Malakoff",
          region: "NY",
          street: "2992 Cameron Road",
          postal_code: "14236",
          country: "US"
        },
        primary: true
      }
    }
  }
};

It has this type.

interface Address {
  readonly city: string;
  readonly region: string;
  readonly street: string;
  readonly postal_code: string;
  readonly country: string;
}

interface AddressData {
  readonly data: Address;
  readonly primary: boolean;
}

interface Owner {
  readonly address: AddressData;
}

interface Account {
  readonly owner: Owner;
}

interface BankIdentity {
  readonly account: Account;
}

Accessing a value

Without using any library I can manipulate this object no problem.
When I want to access a field, I just do the dot syntax and it gives me the
value of that field.

const cityRes =  bankIdentity.account.owner.address.data.city;

// "Malakoff"

Setting a new value and returning the whole object

It becomes a hassle when I have to set a new value.

const cityRes = Object.assign({}, bankIdentity, { 
    account: { 
        owner: { 
            address: { 
                data: Object.assign({}, bankIdentity.account.owner.address.data, { 
                    city: "Another City"
                }) 
            } 
        } 
    }
});

// { account:
//    { owner:
//       { address:
//          { data:
//             { city: 'Another City',
//               region: 'NY',
//               street: '2992 Cameron Road',
//               postal_code: '14236',
//               country: 'US' 
//             } 
//         } 
//      } 
//  } 
//}

Applying a function and returning the whole object

Same ugliness can be seen when applying a function to the field.

const capitalize = (s: string): string => s.toUpperCase();

const cityRes = Object.assign({}, bankIdentity, {
    account: {
      owner: {
        address: {
          data: Object.assign({}, bankIdentity.account.owner.address.data, { 
            city:  capitalize(bankIdentity.account.owner.address.data.city) 
          })
        } 
      } 
    }
  });


// { account:
//    { owner:
//       { address:
//          { data:
//             { city: 'MALAKOFF',
//               region: 'NY',
//               street: '2992 Cameron Road',
//               postal_code: '14236',
//               country: 'US' 
//             } 
//         } 
//      } 
//  } 
//}

monocle-ts

Lenses to the rescue! Unfortunately I don't think it's possible or at least easy
to generate lenses based on the objects like what makeLenses does,
so I have to hand code all of them.

import { Lens } from "monocle-ts";

const account = Lens.fromProp<Bankdentity>()("account");
const owner = Lens.fromProp<Account>()("owner");
const address = Lens.fromProp<Owner>()("address");
const data = Lens.fromProp<AddressData>()("data");
const city = Lens.fromProp<Address>()("city");
const region = Lens.fromProp<Address>()("region");
const street = Lens.fromProp<Address>()("street");
const postalCode = Lens.fromProp<Address>()("postal_code");
const country = Lens.fromProp<Address>()("country");

Well... accessing a value with monocle-ts looks pretty verbose.

const cityRes = account
  .compose(owner)
  .compose(address)
  .compose(data)
  .compose(city)
  .get(bankIdentity);

// "Malakoff"

I guess I can do it like this

const cityRes = Lens.fromPath<BankIdentity>()(["account", "owner", "address", "data", "city"]).get(bankIdentity);

// "Malakoff"

but I think it's better to just use the dot syntax, at least in my opinion.

Lenses shine when it comes to updating and applying a function to a deeply
nested value.

Setting a value and returning the whole object

const cityRes = account
  .compose(owner)
  .compose(address)
  .compose(data)
  .compose(city)
  .set("Another City")(bankIdentity);

// { account:
//    { owner:
//       { address:
//          { data:
//             { city: 'Another City',
//               region: 'NY',
//               street: '2992 Cameron Road',
//               postal_code: '14236',
//               country: 'US' 
//             } 
//         } 
//      } 
//  } 
//}

I think that looks a lot cleaner than using Object.assign.

Applying a function and returning the whole object

Yep. That definitely looks a lot cleaner.

const capitalize = (s: string): string => s.toUpperCase();

const cityRes = account
  .compose(owner)
  .compose(address)
  .compose(data)
  .compose(city).modify(capitalize)(bankIdentity);

// { account:
//    { owner:
//       { address:
//          { data:
//             { city: 'MALAKOFF',
//               region: 'NY',
//               street: '2992 Cameron Road',
//               postal_code: '14236',
//               country: 'US' 
//             } 
//         } 
//      } 
//  } 
//}

References

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (2)

Collapse
 
vitoke profile image
Arvid Nicolaas • Edited

I always liked the idea of immutability and lenses, but disliked the indirectness of the resulting code. The @rimbu/deep library, part of the Rimbu immutable collections library, offers a function called patch and an object called Path that can perform lens-like operations on plain objects.

See:

[Disclaimer] I am the author of Rimbu

Collapse
 
piq9117 profile image
Ken Aguilar

Nice! I'm glad a lot more people are exploring this space in typescript. I use optics coz it's what i'm used to but I gotta admit, the ergonomics isn't that good in typescript.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay