npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

immupdate

v1.3.1

Published

Immutable update for Objects and Arrays

Downloads

10,894

Readme

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.

Why not just recursively deep clone defensively

  • It's very wasteful and can often be too slow for nested structures. immupdate updates the paths that truly changed and only at update time, not defensively everytime an object is handed out in fear it might be mutated in place.
  • If everything gets a new reference, we can't know what changed between two operations.

Why not use Object spreads

  • First, it's terrible for nested updates.
  • it's also pretty bad for shallow updates as it doesn't have the strict semantic of an update operation but rather, the semantic of more generally extending the base object. So while it is technically possible to use it, it's a bad idea and is very unsafe, especially during refactorings. The very least you should expect from a type system is to be able to catch typos. Example:
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:

  • This lib is tiny and only manipulate JS' native data structures. Immutable data structures with a rich API will usually come packaged as a fairly big library.
  • No need for encoding/decoding when the server sends JSON data structures. With immutable data structures, they must first be converted into deeply nested data structures.
  • The client sends back JSON to the server: the deeply nested data structures must be converted back to JSON.
  • Many rendering libraries (mithril, virtual-dom, d3, knockout, etc) expect native Arrays to render a list of DOM nodes. This means our shiny data structure must be converted back to an Array every time a re-render is necessary.
  • Persisting to localStorage is often done via JSON.stringify() for convenience, this is trivial when we are this close to the metal.
  • Popular third party libraries work with plain Objects or Arrays; This might change in a few years when JS has higher level abstractions like iterators available in mainstream browsers and libraries make use of it instead of having a hard dependency on Arrays (or with new languages directly compiling to byte code).
  • Ironically, standard Objects and Arrays are heavily optimized by the JS engines and are often faster and use less memory compared to immutable collections using structural sharing.
  • Any utility functions you may have written for Arrays/Objects will always be portable. Utils for the popular immutable lib of the month will not.

Cons:

  • Immutability can NOT be enforced at compile time as the underlying JS structures are still mutable.
    Coding conventions and discipline is more important than with well designed immutable data structures.

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

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.

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

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')

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}`)

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')

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')

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)