SkillAgentSearch skills...

Icepick

Utilities for treating frozen JavaScript objects as persistent immutable collections

Install / Use

/learn @aearly/Icepick
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

icepick

A tiny (1kb min/gzipped), zero-dependency library for treating frozen JavaScript objects as persistent immutable collections.

Build Status via Travis CI NPM version Coverage Status

Motivation

Object.freeze() is a quick and easy way to get immutable collections in plain JavaScript. If you recursively freeze an object hierarchy, you have a nice structure you can pass around without fear of mutation. The problem is that if you want to modify properties inside this hierarchical collection, you have to return a new copy with the properties changed.

A quick and dirty way to do this is to just _.cloneDeep() or JSON.parse(JSON.stringify()) your object, set the new properties, and re-freeze, but this operation is expensive, especially if you are only changing a single property in a large structure. It also means that all the branches that did not have an update will be new objects.

Instead, what icepick does is provide functions that allow you to "modify" a frozen structure by returning a partial clone using structural sharing. Only collections in the structure that had a child change will be changed. This is very similar to how Clojure's persistent data structures work, albeit more primitive.

icepick uses structural sharing at the object or array level. Unlike Clojure, icepick does not use tries to store objects or arrays, so updates will be less efficient. This is to maintain JavaScript interoperability at all times. Also, for smaller collections, the overhead of creating and managing a trie structure is slower than simply cloning the entire collection. However, using very large collections (e.g.collections with more than 1000 elements) with icepick could lead to performance problems.

Structural sharing is useful wherever you can avoid expensive computation if you can quickly detect if the source data has changed. For example, shouldComponentUpdate in a React component. If you are using a frozen hierarchical object to build a system of React components, you can be confident that a component doesn't need to update if its current props strictly equal the nextProps.

API

  • freeze
  • thaw
  • assoc
  • set
  • dissoc
  • unset
  • assocIn
  • setIn
  • getIn
  • updateIn
  • push
  • unshift
  • pop
  • shift
  • reverse
  • sort
  • splice
  • slice
  • map
  • filter
  • assign
  • extend
  • merge
  • chain

Usage

icepick is provided as a CommonJS module with no dependencies. It is designed for use in Node, or with module loaders like Browserify or Webpack. To use as a global or with require.js, use icepick.min.js or icepick.dev.js directly in a browser.

$ npm install icepick --save
"use strict"; // so attempted modifications of frozen objects will throw errors

var icepick = require("icepick");

The API is heavily influenced from Clojure/mori. In the contexts of these docs "collection" means a plain, frozen Object or Array. Only JSON-style collections are supported. Functions, Dates, RegExps, DOM elements, and others are left as-is, and could mutate if they exist in your hierarchy.

If you set process.env.NODE_ENV to "production" in your build, using envify or its equivalent, freezing objects will be skipped. This can improve performance for your production build.

freeze(collection)

Recursively freeze a collection and all its child collections with Object.freeze(). Values that are not plain Arrays or Objects will be ignored, including objects created with custom constructors (e.g. new MyClass()). Does not allow reference cycles.

var coll = {
  a: "foo",
  b: [1, 2, 3],
  c: {
    d: "bar"
  }
};

icepick.freeze(coll);

coll.c.d = "baz"; // throws Error

var circular = {bar: {}};
circular.bar.foo = circular;

icepick.freeze(circular); // throws Error

thaw(collection)

Recursively un-freeze a collection by creating a partial clone. Object that are not frozen or that have custom prototypes are left as-is. This is useful when interfacing with other libraries.

var coll = icepick.freeze({a: "foo", b: [1, 2, 3], c: {d: "bar"}, e: new Foo() });
var thawed = icepick.thaw(coll);

assert(!Object.isFrozen(thawed));
assert(!Object.isFrozen(thawed.c));
assert(thawed.c !== coll.c);
assert(thawed.e === coll.e);

assoc(collection, key, value)

alias: set

Set a value in a collection. If value is a collection, it will be recursively frozen (if not already). In the case that the collection is an Array, the key is the array index.

var coll = {a: 1, b: 2};

var newColl = icepick.assoc(coll, "b", 3); // {a: 1, b: 3}


var arr = ["a", "b", "c"];

var newArr = icepick.assoc(arr, 2, "d"); // ["a", "b", "d"]

dissoc(collection, key)

alias: unset

The opposite of assoc. Remove the value with the key from the collection. If used on an array, it will create a sparse array.

var coll = {a: 1, b: 2, c: 3};

var newColl = icepick.dissoc(coll, "b"); // {a: 1, c: 3}

var arr = ["a", "b", "c"];

var newArr = icepick.dissoc(arr, 2); // ["a", , "c"]

dissocIn(collection, path)

alias: unsetIn

The opposite of assocIn. Remove a value inside a hierarchical collection. path is an array of keys inside the object. Returns a partial copy of the original collection.

var coll = {a: 1, b: {d: 5, e: 7}, c: 3};

var newColl = icepick.dissocIn(coll, ["b", "d"]); // {a: 1, {b: {e: 7}}, c: 3}

var coll = {a: 1, b: {d: 5}, c: 3};

var newColl = icepick.dissocIn(coll, ["b", "d"]); // {a: 1, {b: {}}, c: 3}

var arr = ["a", "b", "c"];

var newArr = icepick.dissoc(arr, [2]); // ["a", , "c"]

assocIn(collection, path, value)

alias: setIn

Set a value inside a hierarchical collection. path is an array of keys inside the object. Returns a partial copy of the original collection. Intermediate objects will be created if they don't exist.

var coll = {
  a: "foo",
  b: [1, 2, 3],
  c: {
    d: "bar"
  }
};

var newColl = icepick.assocIn(coll, ["c", "d"], "baz");

assert(newColl.c.d === "baz");
assert(newColl.b === coll.b);

var coll = {};
var newColl = icepick.assocIn(coll, ["a", "b", "c"], 1);
assert(newColl.a.b.c === 1);

getIn(collection, path)

Get a value inside a hierarchical collection using a path of keys. Returns undefined if the value does not exist. A convenience method -- in most cases plain JS syntax will be simpler.

var coll = icepick.freeze([
  {a: 1},
  {b: 2}
]);

var result = icepick.getIn(coll, [1, "b"]); // 2

updateIn(collection, path, callback)

Update a value inside a hierarchical collection. The path is the same as in assocIn. The previous value will be passed to the callback function, and callback should return the new value. If the value does not exist, undefined will be passed. If not all of the intermediate collections exist, an error will be thrown.

var coll = icepick.freeze([
  {a: 1},
  {b: 2}
]);

var newColl = icepick.updateIn(coll, [1, "b"], function (val) {
  return val * 2;
}); // [ {a: 1}, {b: 4} ]

assign(coll1, coll2, ...)

alias: extend

Similar to Object.assign, this function shallowly merges several objects together. Properties of the objects that are Objects or Arrays are deeply frozen.

var obj1 = {a: 1, b: 2, c: 3};
var obj2 = {c: 4, d: 5};

var result = icepick.assign(obj1, obj2); // {a: 1, b: 2, c: 4, d: 5}
assert(obj1 !== result); // true

merge(target, source, [associator])

Deeply merge a source object into target, similar to Lodash.merge. Child collections that are both frozen and reference equal will be assumed to be deeply equal. Arrays from the source object will completely replace those in the target object if the two differ. If nothing changed, the original reference will not change. Returns a frozen object, and works with both unfrozen and frozen objects.

var defaults = {a: 1, c: {d: 1, e: [1, 2, 3], f: {g: 1}}};
var obj = {c: {d: 2, e: [2], f: null}};

var result1 = icepick.merge(defaults, obj); // {a: 1, c: {d: 2, e: [2]}, f: null}

var obj2 = {c: {d: 2}};
var result2 = icepick.merge(result1, obj2);

assert(result1 === result2); // true

An optional resolver function can be given as the third argument to change the way values are merged. For example, if you'd prefer that Array values from source be concatenated to target (instead of the source Array just replacing the target Array):

var o1 = icepick.freeze({a: 1, b: {c: [1, 1]}, d: 1});
var o2 = icepick.freeze({a: 2, b: {c: [2]}});

function resolver(targetVal, sourceVal, key) {
  if (Array.isArray(targetVal) && sourceVal) {
    return targetVal.concat(sourceVal);
  } else {
    return sourceVal;
  }
}

var result3 = icepick.merge(o1, o2, resolver);
assert(result === {a: 2, b: {c: [1, 1, 2]}, d: 1});

The resolver function receives three arguments: the value from the target object, the value from the source object, and the key of the value being merged.

Array.prototype methods

  • push
  • pop
  • shift
  • unshift
  • reverse
  • sort
  • splice

Each of these mutative Array prototype methods have been converted:

var a = [1];
a = icepick.push(a, 2); // [1, 2];
a = icepick.unshift(a, 0); // [0, 1, 2];
a = icepick.pop(a); // [0, 1];
a = icepick.shift(a); // [1];
  • slice(arr, start, [end])

slice is also provided as a convenience, even though it does not mutate the original array. It freezes its result, however.

  • map(fn, ar
View on GitHub
GitHub Stars423
CategoryDevelopment
Updated1mo ago
Forks27

Languages

JavaScript

Security Score

95/100

Audited on Feb 18, 2026

No findings