immutable-assign (iassign.js)
Lightweight immutable helper that allows you to continue working with POJO (Plain Old JavaScript Object), and supports full TypeScript type checking.
This library is trying to solve following problems:
- Most immutable JavaScript libraries try to encapsulate the data and provide proprietary APIs to work with the data. They are more verbose than normal JavaScript syntax. E.g., map1.get('b') vs map1.b, nested2.getIn(['a', 'b', 'd']) vs nested2.a.b.d, etc.
- Encapsulated data is no more POJO, therefore cannot be easily used with other libraries, e.g., lodash, underscore, etc.
- Most immutable libraries leak themselves throughout your entire application (including view components), however, they should have been encapsulated at the place where updates happen (e.g., Redux reducers). This is also a pain when you need to change to another immutable library that has its own APIs.
- seamless-immutable address some of above issues when reading the properties, but still use verbose APIs to write properties.
- Immutability Helpers allows us to work with POJO, but it has still introduced some magic keywords, such as $set, $push, etc.
- In addition, we lost TypeScript type checking. E.g., when calling nested2.getIn(["a", "b", "c"]), TypeScript won't be able to warn me if I changed property "c" to "d".
This library is an alternative to Immutable.js, it has only one method iassign(), which accept a POJO object and return you a new POJO object with specific property updated. However, since it works with other libraries such as lodash (refer to example 4), it provides all the functionalities you need plus immutability.
- I have added some options to freeze input and output using deep-freeze, which can be used in development to make sure they don't change unintentionally by us or the 3rd party libraries.
This library will leave your POJO objects completely untouched (except the optional deep-freeze), it does not wrap around nor add any methods/properties to your POJO objects.
This library works in JavaScript and it works really well with TypeScript, because of its generic type argument inference; and since you are working with POJO (not the wrapper objects), you can utilize the full power of TypeScript: IntelliSense, type checking and refactoring, etc.
Performance
Performance of this library should be comparable to Immutable.js, because read operations will always occur more than write operations. When using this library, all your react components can read object properties directly. E.g., you can use <TextBox value={this.state.userinfo.fullName} /> in your components, instead of <TextBox value={this.state.getIn(["userinfo", "fullName"])} />. In addition, shouldComponentUpdate() can compare POJO objects without knowing about the immutable libraries, e.g., return this.props.userInfo.orders !== nextProps.userInfos.orders. I.e., the more read operations you have, the more it will outperform Immutable.js. Following are the benchmarks for multiple immutable libraries (assuming the read to write ratio is 5 to 1):
npm run benchmarks
##Install with npm
npm install immutable-assign --save
Function Signature (TypeScript syntax)
iassign = function<TObj, TProp, TContext>(
obj: TObj,
getProp: (obj: TObj, context: TContext) => TProp,
setProp: (prop: TProp) => TProp,
context?: TContext,
option?: IIassignOption): TObj;
iassign = function<TObj>(
obj: TObj,
setProp: setPropFunc<TObj>,
option?: IIassignOption): TObj;
iassign.fp = function <TObj, TProp, TContext>(
option: IIassignOption,
getProp: getPropFunc<TObj, TProp, TContext>,
setProp: setPropFunc<TProp>,
context?: TContext,
obj?: TObj): TObj;
interface IIassignOption {
freeze?: boolean;
freezeInput?: boolean;
freezeOutput?: boolean;
disableExtraStatementCheck?: boolean;
}
####Example 1: Update object
var iassign = require("immutable-assign");
iassign.freeze = true;
var map1 = { a:1, b:2, c:3 };
var map2 = iassign(
map1,
function (m) { m.b = 50; return m; }
);
####Example 2: Update list/array
var iassign = require("immutable-assign");
var list1 = [1, 2];
var list2 = iassign(
list1,
function (l) { l.push(3, 4, 5); return l; }
);
var list3 = iassign(
list2,
function (l) { l.unshift(0); return l; }
);
var list4 = iassign(
list1,
function (l) { return l.concat(list2, list3); }
);
var list5 = iassign(
list4,
function (l) { return l.sort(); }
);
####Example 3: Update nested structures
var iassign = require("immutable-assign");
var nested1 = { a:{ b:{ c:[3, 4, 5] } } };
var nested2 = iassign(
nested1,
function (n) { return n.a.b; },
function (b) { b.d = 6; return b; }
);
var nested3 = iassign(
nested2,
function (n) { return n.a.b.d; },
function (d) { return d + 1; }
);
var nested4 = iassign(
nested3,
function (n) { return n.a.b.c; },
function (c) { c.push(6); return c; }
);
####Example 4: Work with 3rd party libraries, e.g., lodash
var iassign = require("immutable-assign");
var _ = require("lodash");
var nested1 = { a: { b: { c: [1, 2, 3] } } };
var nested2 = iassign(
nested1,
function (n) { return n.a.b.c; },
function (c) {
return _.map(c, function (i) { return i + 1; });
}
);
var nested3 = iassign(
nested2,
function (n) { return n.a.b.c; },
function (c) {
return _.flatMap(c, function (i) { return [i, i]; });
}
);
####Advanced example 5: Update nested property
var iassign = require("immutable-assign");
var o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }]], c2: {} }, b2: {} }, a2: {} };
var o2 = iassign(
o1,
function (o) { return o.a.b.c[0][0]; },
function (ci) { ci.d++; return ci; }
);
expect(o1).toEqual({ a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }]], c2: {} }, b2: {} }, a2: {} });
expect(o2.a.b.c[0][0].d).toBe(12);
expect(o2).not.toBe(o1);
expect(o2.a).not.toBe(o1.a);
expect(o2.a.b).not.toBe(o1.a.b);
expect(o2.a.b.c).not.toBe(o1.a.b.c);
expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]);
expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]);
expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d);
expect(o2.a2).toBe(o1.a2);
expect(o2.a.b2).toBe(o1.a.b2);
expect(o2.a.b.c2).toBe(o1.a.b.c2);
expect(o2.a.b.c[0][0].e).toBe(o1.a.b.c[0][0].e);
expect(o2.a.b.c[1][0]).toBe(o1.a.b.c[1][0]);
####Advanced example 6: Update array
var iassign = require("immutable-assign");
var o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }]], c2: {} }, b2: {} }, a2: {} };
var o2 = iassign(
o1,
function (o) { return o.a.b.c[1]; },
function (c) { c.push(101); return c; }
);
expect(o1).toEqual({ a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }]], c2: {} }, b2: {} }, a2: {} });
expect(o2.a.b.c[1][1]).toBe(101);
expect(o2).not.toBe(o1);
expect(o2.a).not.toBe(o1.a);
expect(o2.a.b).not.toBe(o1.a.b);
expect(o2.a.b.c).not.toBe(o1.a.b.c);
expect(o2.a.b.c[1]).not.toBe(o1.a.b.c[1]);
expect(o2.a2).toBe(o1.a2);
expect(o2.a.b2).toBe(o1.a.b2);
expect(o2.a.b.c2).toBe(o1.a.b.c2);
expect(o2.a.b.c[0]).toBe(o1.a.b.c[0]);
expect(o2.a.b.c[0][0]).toBe(o1.a.b.c[0][0]);
expect(o2.a.b.c[1][0]).toBe(o1.a.b.c[1][0]);
####Advanced example 7: Update nested property, referring to external context.
var iassign = require("immutable-assign");
var o1 = { a: { b: { c: [{ d: 11, e: 12 }, { d: 21, e: 22 }] } } };
var external = { a: 0 };
var o2 = iassign(
o1,
function (o, ctx) { return o.a.b.c[ctx.external.a]; },
function (ci) { ci.d++; return ci; },
{ external: external }
);
expect(o1).toEqual({ a: { b: { c: [{ d: 11, e: 12 }, { d: 21, e: 22 }] });
expect(o2.a.b.c[external.a].d).toBe(12);
expect(o2).not.toBe(o1);
expect(o2.a).not.toBe(o1.a);
expect(o2.a.b).not.toBe(o1.a.b);
expect(o2.a.b.c).not.toBe(o1.a.b.c);
expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]);
expect(o2.a.b.c[0].d).not.toBe(o1.a.b.c[0].d);
expect(o2.a.b.c[0].e).toBe(o1.a.b.c[0].e);
expect(o2.a.b.c[1]).toBe(o1.a.b.c[1]);
expect(o2.a.b.c[1].d).toBe(o1.a.b.c[1].d);
expect(o2.a.b.c[1].e).toBe(o1.a.b.c[1].e);
####Example 8: Update nested structures using iassign.fp() and currying
var iassign = require("immutable-assign");
var nested1 = { a: { b: { c: [3, 4, 5] } } };
var iassignFp = iassign.fp(undefined)
(function (n) { return n.a.b; })
(function (b) { b.d = 6; return b; })
(undefined);
var nested2 = iassignFp(nested1);
iassignFp = iassign.fp(undefined)
(function (n) { return n.a.b.d; })
(function (d) { return d + 1; })
(undefined);
var nested3 = iassignFp(nested2);
iassignFp = iassign.fp(undefined)
(function (n) { return n.a.b.c; })
(function (c) { c.push(6); return c; })
(undefined);
var nested4 = iassignFp(nested3);
iassignFp = iassign.fp(undefined)
(function (n, ctx) { return n.a.b.c[ctx.i]; })
(function (ci) { return ci + 100; })
({i: 1});
var nested5 = iassignFp(nested4);
##Constraints
- getProp() must be a pure function; I.e., it cannot access anything other than the input parameters. e.g., it must not access "this" or "window" objects. In addition, it must not modify the input parameters. It should only return a property that needs to be updated.
- getProp() currently does not support comments in the function body, you can work around this by putting comments outside of the function body.
##History
- 1.0.29 - Supported ES6 Arrow Functions
- 1.0.27 - Added iassign.fp() that support currying, refer to example 8
- 1.0.26 - Works with webpack, please refer to ImmutableAssignTest
- 1.0.23 - Greatly improved performance.
- 1.0.21 - Added new function overload to skip getProp() if you trying to update the root object, refer to example 1 and example 2
- 1.0.20 - Added Travis-CI, Coveralls (coverage) and SauceLabs (browsers' tests)
- 1.0.19 - Added TypeScript types to package.json
- 1.0.18 - Tested on Mac (Safari 10 and Chrome 54)
- 1.0.16 - Tested in Node.js and major browsers (IE 11, Chrome 52, Firefox 47, Edge 13, PhantomJS 2)