basic-immutable
An unsophisticated, single-dependency, TypeScript friendly immutability library for creating backwards JavaScript compatible objects and arrays, with a few extra features just for fun. Under the hood, this library is really just an immutable extension of several Lodash methods, thrown together quickly and easily; nothing too fancy here 😄
Contents
Background
For the million or so immutability libraries out there, surprisingly, I was not able to find one that suited my needs perfectly. So I'm adding one more to the mix. Here's a brief summary of my favorite features of BasicImmutable, that I've found disparately across several libraries, but never together in one:
- Very TypeScript friendly (I had difficulty with ImmutableJS in particular for this one). Great for working with Redux apps in TypeScript, as complex types can be assigned to your state objects just as if you were working with a plain JavaScript object
- Completely backwards JavaScript compatible, use BasicImmutable data structures in your code like any other JavaScript objects
- Re-implements all native JavaScript array mutator methods as immutable; e.g.
Array.push()
returns a new array with items appended, instead of returning the length of the array. - Ability to easily strip away the API (with
toJS
), and convert to a 100% plain-old JavaScript object, in case you still find edge cases in which there are unintended side-effects (such as in AngularJS, where using other libraries broke certain functionalities for me) - Relatively small API surface area to learn; in addition to understanding that all array mutator methods are now immutable, there are only about 10 BasicImmutable specific methods to learn. Most of which should be very familiar to you if you've ever used Lodash.
However, that this library is so tailored to my needs, means that it might not be right for you, or you may simply be happier with one of the alternatives. And there has not been any special attention paid to performance or speed - the library is as fast as Lodash is. Try it out if you'd like, otherwise, no big deal. My intent in publishing this, was just in case even one other person out there was facing the same struggles as I was, they might find this useful.
Getting Started
Installation
- To install, run
yarn add basic-immutable
ornpm install --save basic-immutable
- TypeScript typings are included and do not need to installed separately
Pass your data structure to Immutable()
and call toJS()
to transform them back into POJO (plain-old JavaScript objects). Full list of methods and examples can be found in the API section below.
Basic examples
ImmutableObject
with TypeScript:
; ; .merge .set'users[2]', ;
ImmutableArray
with JavaScript:
const userStatus = ; const userStatus1 = userStatus; // returns new ImmutableArray, not length const userStatus2 = userStatus1;// the original userStatus is unchanged // is just an array with extended functionality// can still iterate over as normalfor let el of userStatus2 console// prints:// 'Brittany' true// 'Jitendra' true// 'Mikael' false// 'Maria' true
API
Available Methods:
- ImmutableObject / ImmutableArray
- ImmutableObject
- ImmutableArray
- pull
- flatten
- toObject
- all methods available on plain old JavaScript arrays**
BasicImmutable
arrays
**An important note about - Arrays created with
Immutable()
are still just JavaScript arrays under the hood, which means that all of the native array methods are still available to you, and all non-mutator methods behave as normal (filter
,map
,reduce
,includes
,indexOf
, etc.). However, all mutator methods have been re-implemented as immutable. - For example,
Immutable([1, 2, 3]).push(1)
, rather than returning the array's length after performing the push operation, will instead return a newBasicImmutable
array (the original array is, of course, unmutated).
A full list of the re-implemented non-mutator methods is below, which all return new BasicImmutable arrays:
Method | Description |
---|---|
push | Returns a new BasicImmutable array with one or more elements added to the end |
pop | Returns a new BasicImmutable array with the last element removed |
shift | Returns a new BasicImmutable array with the first element removed |
unshift | Returns a new BasicImmutable array with one or more elements added to the front |
splice | Returns a new BasicImmutable array with elements added and/or removed |
sort | Returns a new BasicImmutable array with the elements sorted |
reverse | Returns a new BasicImmutable array with the order of the elements reversed |
copyWithin | Returns a new BasicImmutable array with a sequence of array elements copied within the array |
fill | Returns a new BasicImmutable array filled from a start index to an end index with a static value |
BasicImmutable & TypeScript
An important note about working with BasicImmutable
and TypeScript: All methods that return a new, modified object or array will accept type arguments via the generic versions of their typings, so that a new type can be assigned should the given operation change the shape of your object or array.
For example:
; // inferred type of obj1 is still ImmutableObject<{ a: number; b: number; }>
This is incorrect. To ensure the object maintains type safety and accuracy, use the generic form of the method:
; // type is now ImmutableObject<{ b: number; }>, which is correct
This might also be useful for converting an array's type, when popping, pushing, or otherwise modifying the returned array.
; // create new array and assign tuple-like type of ImmutableArray<'Jared' | 'online> ; // ['Jared','online']
This approach will work for all modifying methods: set
, update
, delete
, merge
(ImmutableObject only), mergeTolerant
(ImmutableObject only), pull
(ImmutableArray only), fill
(ImmutableArray only), etc.
Of course, if your object or array is less strictly typed to begin with, this could be a non-issue, so there is a balancing act between type-safety and what works best for you and your particular use-case:
// OK! but not super type-safe; // inferred type of obj1 is still ImmutableObject<{ [key: string]: number; }>
get(path)
Gets the value at a given path of an ImmutableObject
or ImmutableArray
and returns it. Uses Lodash's _.get()
.
Parameters:
path
Number, path string, or path array
const arr = ;const obj = ; console; // hconsole; // o console; // dconsole; // oconsole; // r
☝️ Run ⬆️ Back to Methods List
set(path, value)
Returns a new ImmutableObject
or ImmutableArray
with value set at the given path. Uses Lodash's _.set()
.
Parameters:
path
Number, path string, or path array
value
The value to set at the resolved path
const arr = ;const obj = ; const obj1 = obj;const obj2 = obj; const arr1 = arr;const arr2 = arr1;
☝️ Run ⬆️ Back to Methods List
update(path, updater)
Like set
except accepts an updater function to produce the value to set. Uses Lodash's _.update()
.
Parameters:
path
Number, path string, or path array
updater
The function to produce the updated value
const arr = ;const obj = ; const obj1 = obj;const obj2 = obj1;const arr1 = arr;const arr2 = arr1;
☝️ Run ⬆️ Back to Methods List
delete(path)
Returns a new ImmutableObject
or ImmutableArray
with element at a given path or position removed.
TypeScript Tip: To maintain type-safety, be careful to re-type your object with the delete
generic if the deletion causes a conflict with the base type originally defined or inferred for you object. See the note here about working with BasicImmutable and TypeScript.
Parameters:
path
Number, path string, or path array
With TypeScript:
// delete nested elements; // re-assign type; // { a: 1, b: 2 } ;
With JavaScript:
const obj = const arr = ; // delete nested elementsconst obj1 = obj; // { a: 1, b: [ 1, 2, { y: 2 }]}const arr1 = arr; // [1, 2, [3, 5]]
☝️ Run ⬆️ Back to Methods List
equals(comparison)
Performs a deep comparison (excluding the basic-immutable methods) between the object and array and the given argument. Uses Lodash's _.equals()
.
Parameters:
comparison
Any (but an object or array makes the most sense).
const arr = ;const obj = ; arr; // trueobj; // false
☝️ Run ⬆️ Back to Methods List
asMutable
Converts to mutable object or array, i.e. basic-immutable methods are still available, but mutations are allowed until another basic-immutable modifying method is called (i.e. any method that returns a new ImmutableObject
or ImmutableArray
).
NOTE: For arrays, methods that are traditionally mutator methods will still be performed immutably, and if called, will return the array to it's immutable state.
Parameters: None
const arr = ;const obj = ; const arr1 = arr;arr10 = 'allowed';console; // 'allowed' const arr2 = arr1;arr20 = 'not allowed'; // nope!// push() was called, the resulting array is immutable const obj1 = obj;obj1a = 2; // { a: 2, b: 2 }
☝️ Run ⬆️ Back to Methods List
toJS(frozen?)
Converts ImmutableObject
or ImmutableArray
to plain-old JavaScript and returns it (can be mutated, basic-immutable API helper methods are stripped away. To return a plain, but frozen object, pass true
).
Useful for working with front-end libraries that don't play well with instance objects (such as AngularJS, which I had difficulties with in particular when trying to use other plain-old JS immutability libraries).
Parameters:
frozen
[optional] boolean, whether or not to return a frozen object or array.
const arr = ;const obj = ; const jsArr = arr;const jsObj = obj; let props = Object;props = props; const isDefinitelyJustJS = !props; // true jsObja = 3; // can be mutatedjsArr3 = 4; // can be mutated const frozenJSObj = obj; // plain-jane JS, but cannot be mutatedfrozenJSObja = 3; // throws in strict mode
☝️ Run ⬆️ Back to Methods List
merge(...sources)
Merges source object(s) into target object and returns updated target as a new ImmutableObject
. Will throw if any of the source objects contain keys uncommon to the target object. This is valuable to prevent accidentally merging the wrong source. TIP: Use TypeScript to catch this error before runtime! Uses Lodash's _.merge()
.
Use mergeTolerant
for a less strict merge.
Parameters:
source
or …sources
One or more source objects.
With TypeScript:
; // { a: 'bar', b: 3 } obj.merge// Error! [TS] Object literal may only specify known properties, and 'c' does not exist in type 'Partial<{ a: string; b: number; }>' obj.merge// Error! [TS] Argument of type '{ b: string; }' is not assignable to parameter of type 'Partial<{ a: string; b: number; }>'
With JavaScript:
const obj = ; const obj1 = obj;// { a: 3, b: 2, c: 3 } const obj2 = obj1;// { a: 3, b: 4, c: 5 } obj; // throws! key 'd' is not found on source object
☝️ Run ⬆️ Back to Methods List
mergeTolerant(...sources)
Like merge
, except this method allows source objects of different shapes (containing keys uncommon to the target object) to be merged. Uses Lodash's _.merge()
.
Typescript Tip: If using TypeScript, have no fear, this will still be a type-safe operation. The typings instruct TypeScript to create a new intersection type from the type of your original object and the type of the source object or objects. Be explicit to avoid loose type inference by passing your new type(s) to the mergeTolerant
generic (see example below).
Parameters:
source
or …sources
One or more source objects.
With TypeScript:
; ;// is inferred type { a: "yes"; b: "no"; } & { c: string; } ;// is safer type { a: "yes"; b: "no"; } & { c: "maybe"; }
With JavaScript:
const obj = ; const obj1 = obj;// { a: 1, b: 2, c: 3 }
☝️ Run ⬆️ Back to Methods List
toArray
Returns a new ImmutableArray
array containing the values of the object's keys.
TypeScript Tip: If using TypeScript, and your object has been explicitly typed, e.g. Immutable<{ a: 1, b: 2 }>({a: 1, b: 2});
, and no type argument is explicitly passed to the toArray
generic, this method will return a tuple-like array, which is strictly typed to allow only the values found in your object. For a less strictly typed array, pass a type argument, e.g. toArray<number>();
, this way you will be able to add additional data with the type number
to your array.
Parameters: None
With TypeScript:
;// inferred type: ImmutableArray<1, 2> // will allow additional 1s and 2s to be addedarr.push1, 1, 2, 2; // OK! // but attempting to add other values will cause an errorarr.push3; // Error!// [ts] Argument of type '3' is not assignable to parameter of type '1 | 2'. ;// type: ImmutableArray<number>, i.e. number[]arr1.push3, 4, 5; // OK! // because obj1's type was inferred, not explicitly defined
With JavaScript:
const obj = ; const arr = obj; // [1, 2, 3]
☝️ Run ⬆️ Back to Methods List
pull(deep, ...values)
Removes all provided values from an ImmutableArray
using SameValueZero for equality comparisons, either from the top level array, or recursively. Uses Lodash's _.pull()
.
Parameters:
pullDeep
Indicates whether or not to pull value(s) only from the top level array, or to pull value(s) recursively.
With TypeScript:
;// inferred type ImmutableArray<number | string>, i.e. (number | string)[] // pass type argument to re-type the ImmutableArray; // [1, 2, 3]// now ImmutableArray<number>, i.e. number[]
With JavaScript:
const arr = ; const arr1 = arr; // [ 1, 4, [ 1, 3, 3, 4, [ 1, 3 ] ], 1 ] const arr2 = arr1; // [ 1, [ 1, [ 1 ] ], 1 ]
☝️ Run ⬆️ Back to Methods List
flatten(deep?)
Flattens ImmutableArray
either a single level deep or recursively. Uses Lodash's _.flatten()
.
Parameters:
deep
[optional] boolean, indicates whether or not to flatten recursively.
With TypeScript:
;// inferred type ImmutableArray<number | (number | number[])[]> ; // [0, 0, 1, 1, 2, 2, [3, 3]] // pass type argument to re-type the ImmutableArray;// now ImmutableArray<number>, i.e. number[]
With JavaScript:
const arr = ; const flat = arr; // [0, 0, 1, 1, 2, 2, [3, 3]] const flatter = arr; // [0, 0, 1, 1, 2, 2, 3, 3]
☝️ Run ⬆️ Back to Methods List
toObject(keyInitializer?)
Maps over array and converts to ImmutableObject
. Keys-value pairs are produced by the elements in the array (values) and by providing a key-initializer argument as described below (keys).
Parameters:
keyInitializer
[optional] A single-letter string, equivalent to /[a-zA-Z]/
, number, or function synonymous with an Array.map
callback.
- If provided a letter or number, keys will begin with that letter or number and be incremented for each element of the array.
- To designate custom keys, provide an
Array.map
callback instead (see example below). - If undefined, keys will be equal to their values (numbers) or the stringified version of their values. Both
number
andstring
key indexers will be allowed.
With TypeScript:
;; /******** with no `keyInitializer` argument: ***********/;// { a: 'a', b: 'b', c: 'c', d: 'd' }// pass type argument to indicate correct key indexer, default is { [key: number]: T; [key: string]: T; } /******** with string `keyInitializer` argument: ***********/;// { A: [1, 2], B: [3, 4], C: [5, 6], D: [7, 8] } // same as above with more strict typing;;// { A: [1, 2], B: [3, 4], C: [5, 6], D: [7, 8] } /******** with number `keyInitializer` argument: ***********/;// { 1: [1, 2], 2: [3, 4], 3: [5, 6], 4: [7, 8] } /******** with Array.map `keyInitializer` argument: ***********/;// { 3: [1, 2], 7: [3, 4], 11: [5, 6], 15: [7, 8] } ;// { A: 'a', B: 'b', C: 'c', D: 'd' }
With JavaScript:
const arr = ;const nested = ; /******** with no `keyInitializer` argument: ***********/const obj1 = arr;// { a: 'a', b: 'b', c: 'c', d: 'd' } /******** with string `keyInitializer` argument: ***********/const obj2 = nested;// { A: [1, 2], B: [3, 4], C: [5, 6], D: [7, 8] } /******** with number `keyInitializer` argument: ***********/const obj3 = nested;// { 1: [1, 2], 2: [3, 4], 3: [5, 6], 4: [7, 8] } /******** with Array.map `keyInitializer` argument: ***********/const obj4 = nested;// { 3: [1, 2], 7: [3, 4], 11: [5, 6], 15: [7, 8] } const obj5 = arr;// { A: 'a', B: 'b', C: 'c', D: 'd' }
☝️ Run ⬆️ Back to Methods List
Contributing
I slapped this together pretty quick, and it's not meant to be fancy or a miraculous feat of programming. That said, I'm sure there's still plenty of room for improvement. Contributing guidelines will be added soon, but for now, if there's any interest in contributing, please open an issue!