jamesmcnamara / shades

A lodash-inspired lens-like library for Javascript
MIT License
412 stars 14 forks source link

set to undefined path #23

Closed oluckyman closed 5 years ago

oluckyman commented 5 years ago

Thanks for the awesome lib! I have a question. It's inspired by lodash but set fails to create missing objects to accomplish the task:

const user = {
  name: 'Misha'
}
const newUser = set('address', 'zip')('1234')(user)

Fails with "Cannot read property 'set' of undefined" Is it by design? How one supposed to handle such situation? This will work, but does not seems elegant at all 🙁

let newUser = user;
if (!user.address) {
  newUser = set('address')({})(newUser)
}
newUser = set('address', 'zip')('1234')(user)
jamesmcnamara commented 5 years ago

Good question. This is actually by design. For one, if shades automatically created objects when they didn't exist, it would be harder to catch errors like:

set('adress', 'zip')('1234')(user)

For another, we don't know that creating an object is even the right behavior. Maybe the user wants a Map, or an Immutable.js Record. Keeping this flexible enough to handle all of these options would add too much complexity and mess to the API.

Finally, this would be very difficult for the type system to handle.

In terms of workarounds, there's two approaches that are best (in my opinion).

1. When you know the value doesn't exist

This is the easiest. All you have to do is stop the lens at the last point where you have guaranteed keys:

set('address')({zip: '1234'})(user)

Clean and simple. However, if you had a value at address with other keys that you wanted to preserve, they would be blown away.

Which brings us to...

2. When you want to set values, preserve existing, and handle null

For this case, you can use the new function fill (only available on the beta).

> mod('address')(fill({zip: '1234'}))({})
{ address: { zip: '1234' } }

However, this does have a downside. fill always defaults to the value in the given object, so if the value does have a zip, then it will be maintained:

> mod('address')(fill({zip: '1234'}))({address: {zip: '4567'})
{ address: { zip: '4567' } }

To fix this, we just have to flip the order of arguments to fill:

> mod('address')(flip(fill)({zip: '1234'}))({address: {zip: '4567'})
{ address: { zip: '1234' } }

This approach also merges in keys from both objects in the ways you would want:

> const backfill = flip(fill)
> const user = {address: {zip: '4567', city: 'London'}
> mod('address')(backfill({zip: '1234'}))(user)
{ address: { zip: '1234', city: 'London' } }