Home

Awesome

immupdate_logo

immutable update utils for JS Object and Arrays.

This library only does simple updates (e.g setting, modifying or deleting a value), for more powerful combinators, see space-lift which also fully includes immupdate.

<a name="intro"></a>

Why not just recursively deep clone defensively

Why not use Object spreads

const john = {name: 'john', address: '17, claproast st'};
const updatedJohn = {...john, adress: '18, claproast st'}; // oh no

Why Object/Array instead of immutable data structures

Pros:

Cons:

<a name="howtouse"></a>

How to use

Here's everything that can be imported from immupdate:

import { update, deepUpdate, DELETE } from 'immupdate'

update updates the shallow properties of an object
deepUpdate can update one arbitrarily nested property in a JSON tree
DELETE is a special marker used with update and immupdate to delete a property

Examples

<a name="update-multiple-properties"></a>

Update multiple properties

import { update, DELETE } from 'immupdate'

type Person = { id: number, name: string, tatoo?: string }

const jose: Person = {
  id: 33,
  name: 'Jose',
  tatoo: '自由'
}

const carla = update(jose, {
  name: 'Carla',
  tatoo: DELETE
})

The differences with Object.assign or the { ...source } notation is that you can delete a property and update() is typesafe: You can't make a typo, add completely new keys, etc.

<a name="update-nested-property"></a>

Update a nested property

Two ways to do it:

  1. update performs shallow updates, so we will just have to call it multiple times.
import { update } from 'immupdate'

interface Person {
  id: string
  prefs: {
    csvSep: ',' | ';'
    timezone: number
    otherData?: {
      nestedData: {}
    }
  },
  friends: number[]
}

const person = {
  id: 33,
  prefs: {
    csvSep: ',',
    timezone: 2,
    otherData: {
      nestedData: {}
    }
  },
  friends: [1, 2, 3]
}

const newPrefs = update(person.prefs, { csvSep: ';' })

const personWithNewPrefs = update(person, { prefs: newPrefs })
  1. but this can get tedious pretty fast, especially once we reach 3 levels of nesting or more. deepUpdate to the rescue:
import { deepUpdate } from 'immupdate'

const personWithNewPrefs = deepUpdate(person)
  .at('prefs')
  .at('csvSep')
  .set(';')

This is fonctionally equivalent to the code using multiple update calls.

person was only updated where necessary. Below in green are the paths that were updated. This is much more efficient than deep cloning.
update

<a name="update-array-item"></a>

Update an Array item

Two choices:

  1. Using update which doesn't specifically deal with Arrays, so we want to prepare the updated Array instance beforehand.
import { update } from 'immupdate'

const person = {
  friends: [
    { id: 1, name: 'biloute' },
    { id: 2, name: 'roberto' },
    { id: 3, name: 'jesus' }
  ]
}

const newFriends = person.friends.map(f => f.id === 3 ? update(f, { name: 'rocky' }) : f)

const newPerson = update(person, { friends: newFriends })
  1. Using deepUpdate:
import { deepUpdate } from 'immupdate'

const newPerson = deepUpdate(person)
  .at('friends')
  .at(person.friends.findIndex(f => f.id === 3))
  .abortIfUndef()
  .at('name')
  .set('rocky')

<a name="update-nested-property-modify"></a>

Modify a nested property using its current value

import { deepUpdate } from 'immupdate'

const person = {
  friends: [
    { id: 1, name: 'biloute' },
    { id: 2, name: 'roberto' },
    { id: 3, name: 'jesus' }
  ]
}

const newPerson = deepUpdate(person)
  .at('friends')
  .at(person.friends.findIndex(f => f.id === 3))
  .abortIfUndef()
  .at('name')
  .modify(name => `MC ${name}`)

<a name="update-nested-nullable-property"></a>

Update a nested property on a nullable path

If there is a nullable key (or An array index being used) anywhere in the update path and it's not the last key (i.e, right before modify or set)
then it's a potentially unsafe operation and the library will ask you to decide what to do with it (it won't compile until you do).

You can either:

  1. Abort the whole thing (the returned value is the same as the passed one)
import { deepUpdate } from 'immupdate'

interface Person {
  prefs?: {
    lang: string
  }
}

deepUpdate<Person>({})
  .at('prefs')
  .abortIfUndef()
  .at('lang')
  .set('en')

Or

  1. Specify a default value to be used if the current value is null/undefined
import { deepUpdate } from 'immupdate'

interface Person {
  prefs?: {
    lang: string
  }
}

const defaultPrefs = { lang: 'en' }

deepUpdate<Person>({})
  .at('prefs')
  .withDefault(defaultPrefs)
  .at('lang')
  .set('en')

<a name="update-nested-union-property"></a>

Update a nested union property

import { deepUpdate } from 'immupdate'

type A = { type: 'a', data: string }
type B = { type: 'b', data: number }
type Container = { aOrB: A | B }
const isA = (u: A | B): u is A => u.type === 'a'

const container = { aOrB: { type: 'a', data: 'aa' } }

deepUpdate(container)
  .at('aOrB')
  .abortIfNot(isA)
  .at('data')
  .set('bb')

<a name="update-option"></a>

Update a space-lift Option

Additionally, if you're also using space-lift, you can update Option values anywhere in a tree. Updating an Option<T> works exactly like updating a T | undefined, so you still have to explicitly tell deepUpdate what to do in case it encounters a None.

const obj = {
  a: Some({
    b: Some({ c: 1 }),
    x: { y: 'y' }
  })
}

const result = deepUpdate(obj)
  .at('a')
  .abortIfUndef()
  .at('b')
  .withDefault({ c: 5 })
  .at('c')
  .set(10)