An immutable, functional data collection library for plain old Javascript.
It combines my favourite parts of immutable.js, lodash/fp and ramdajs together.
Unmutable follows immutable.js's API almost exactly, and but exports each method individually as a point-free function, in the same way as lodash/fp does. These functions are all immutable, and can be composed together easily.
Unmutable also works seamlessly on plain Javascript objects and arrays, Immutable.js Maps, Lists and Records.
var data = [1,2,3];
let result = pipeWith(
data,
push(4),
reverse(),
map(num => num * 10)
);
// result is [40,30,20,10]
// wow!var data = {
foo: 1,
bar: null,
baz: 3
};
var filterNulls = filter((val) => {
return val === null;
});
let result = filterNulls(data);
// result is {foo: 1, baz: 3}
// golly!var data = [1,3,3,2,1];
let result = unique()(data);
// result is [1,3,2]
// yee!All functions can be imported from unmutable/<functionName>
import getIn from 'unmutable/getIn';
import map from 'unmutable/map';
import filter from 'unmutable/filter';set(key: string|number, value: any) => (collection) => newCollectionReturns a new collection also containing the new key, value pair. If an equivalent key already exists in this collection, it will be replaced.
When used with an indexed data type, key may be a negative number, which indexes back from the end of the collection.
If index larger than size, the returned collections's size will be large enough to include the index.
delete(key: string|number) => (collection) => newCollectionReturns a new collection which excludes this key.
When used with an indexed data type, this returns a new collection which excludes this index and with a size 1 less than this collection. Values at indices above index are shifted down by 1 to fill the position.
deleteAll(key) => (collection) => newCollectionReturns a new collection which excludes the provided keys.
pick(keys: string[]|number[]) => (collection) => newCollectionReturns a new collection which excludes all keys that aren't listed in keys.
clear() => (collection) => newCollectionReturns a new collection containing no keys or values.
push(...values: Array<any>) => (collection) => newCollectionReturns a new collection with the provided values appended, starting at this collection's size.
pop() => (collection) => newCollectionReturns a new collection with a size ones less than this collection, excluding the last index in this collection.
unshift(...values: Array<any>) => (collection) => newCollectionReturns a new collection with the provided values prepended, shifting other values ahead to higher indices.
shift() => (collection) => newCollectionReturns a new collection with a size ones less than this collection, excluding the first index in this collection, shifting all other values to a lower index.
move(fromIndex: number, toIndex: number) => (collection) => newCollectionMoves the element at fromIndex to the position of toIndex.
update(key: string|number, notSetValue: any, updater: (value: any) => any) => (collection) => newCollection
update(key: string|number, updater: (value: any) => any) => (collection) => newCollection
update(updater: (collection: any) => any) => (collection) => newCollectionReturns a new collection having updated the value at this key with the return value of calling updater with the existing value.
updateInto(key: string|number, updater: (collection) => newValue) => (collection) => newCollectionPasses collection to updater, and will set collection[key] to the result of the updater. In practise it works a bit like update(key, updater), but in this case updater receives collection instead of collection[key].
merge(...otherCollections: Array<Collection>) => (collection) => newCollectionReturns a new collection resulting from merging the provided otherCollections into this collection. In other words, this takes each entry of each otherCollection and sets it on this collection.
mergeWith(
merger: (oldValue: any, newValue: any, key: string) => value,
...otherCollections: Array<Collection>) => (collection) => newCollection
)Like merge(), mergeWith() returns a new collection resulting from merging the provided otherCollections into this collection, but uses the merger function for dealing with conflicts.
setSize(size: number) => (collection) => newCollectionReturns a new collection with size size. If size is less than this collection's size, the new collection will exclude values at the higher indices. If size is greater than this collection's size, the new collection will have undefined values for the newly available indices.
rename(oldKey: string|number, newKey: string|number) => (collection) => newCollectionChanges the key of oldKey to the key of newKey
swap(keyA: string|number, keyB: string|number) => (collection) => newCollectionSwaps the values at the given keys. Keys that don't exist are assumed to have a value of undefined.
rotate(shift: number) => (collection) => newCollectionRotates the elements in arrays and Lists around, according to the value of shift. A positive shift will move elements to the left, appending rotated elements to the end of the array, where as a negative shift will move elements to the right, prepending rotated elements to the start of the array.
unit(otherCollection) => (collection) => newCollectionAttempts to turn otherCollection into collections data type, and returns newCollection.
setMutate(key: string|number, value: any) => (collection) => alteredCollectionLike set() but performs mutations on value for objects and arrays. It is less expensive than set().
deleteMutate(key: string|number, value: any) => (collection) => alteredCollectionLike delete() but performs mutations on value for objects and arrays. It is less expensive than delete().
map(mapper: (value: any, key: string|number, original: any) => newValue) => (collection) => newCollectionReturns a new collection with values passed through a mapper function.
mapKeys(mapper: (key: string|number, value: any, original: any) => newKey) => (collection) => newCollectionReturns a new collection with keys passed through a mapper function.
mapEntries(mapper: ([key, value]: any, index: number, original: any) => [newKey, newValue]) => (collection) => newCollectionReturns a new collection with key value pairs passed through a mapper function.
flip() => (collection) => newCollectionReturns a new collection with its key values pairs flipped, so values become keys and keys become values
stableSortBy(comparatorValueMapper: value => valueToCompare, comparator?: Function) => (collection) => newCollectionLike sortBy(), but guarantees that items are stable sorted, meaning that the order of items that produce equivalent sort values are retained.
sortLike(
idCollection,
idMapper: (item) => id,
unmatchedIds?: boolean,
unmatchedItems?: boolean
) => (collection) => newCollectionThis will sort collection so it matches the order of ids specified in idCollection. The idMapper function is used to generate ids for each item in collection, which are then compared against idCollection.
Any ids in idCollection that have no match in collection will not be output, or if unmatchedIds is true then they will be output as undefined.
If unmatched is true, all items in collection that were not matched by idCollection will be output at the end of the returned collection, in the order they appear in the input collection.
chunk(size: number) => (collection) => newCollectionReturns an array of 'chunks'. This function splits collection up into chunks, where each chunk is of the same type as collection, and contains size number of values.
chunkBy(predicate: Function) => (collection) => newCollectionReturns an array of 'chunks'. This function splits collection up into chunks, where the size of each chunk is determined by the predicate. It iterates over collection and calls predicate for each item on the collection. Whenever predicate returns true, a new chunk is started. It returns an array containing all the chunks that were created.
deal(groups: number) => (collection) => newCollectionReturns an array of 'chunks'. This function iterates over collection, dividing it into the number of groups specified by the groups argument. It works in a similar way to someone dealing out cards to a number of players, putting the first item in the first group, the second item in the second group etc. Once the last group is reached, the next item is put in the first group again, and the deal continues cyclically until no items are left.
keyBy(keyer: (value) => string) => (value) => {[key: string]: value} // ObjectIterates over collection and calls keyer on each item, and using the result as a key on the output object. TThe corresponding value of each key is the last element responsible for generating the key.
unique() => (collection) => newCollectionFilters collection so that any element with a duplicate value is filtered out. If the value is a collection it is compared deeply.
uniqueBy(getter: (collection) => any) => (collection) => newCollectionFilters collection according to the result of getter, so that any element with a duplicate result of getter is filtered out. If a collection is returned from getter, it is compared deeply.
toIndexed() => (collection) => newCollectionConverts plain Javascript data types into arrays, and converts Immutable.js objects into Lists
toJSON() => (collection) => anyTurns the collection into plain Javascript if it is an Immutable.js data type. Internally if collection is a List then .toArray() is called, and if collection is a Map then toObject() is called.
toKeyed() => (collection) => newCollectionConverts plain Javascript data types into objects, and converts Immutable.js objects into Maps.
toEntries() => (collection) => newCollectionConverts collections into arrays of key, value tuples.
pivot() => (collection) => newCollectionPivots the collection. The keys at the first level of nesting are moved to the second level, and the keys of the second level are moved to the first.
`equals(otherValue) => (value) => boolean`Returns true if value and otherValue are deeply equal, or false otherwise.
notEquals(otherValue) => (value) => booleanReturns true if value and otherValue are not deeply equal, or false otherwise.
strictEquals(otherCollection: any) => (collection: any) => numberChecks if collection and otherCollection are strictly equal. This complements equals(), which checks for deep value equality.
shallowEquals(otherCollection: any) => (collection: any) => numberChecks if collection and otherCollection are shallowly equal, using strict equality.
`equalsType(otherValue: any) => (value: any) => boolean`Returns true if value and otherValue are of the same type, or false otherwise. Class instances only count as equal if they are instances of the same class. Also unlike the type returned by typeof, null is not equal to objects, and are only equal to null.
entriesReverse() => (collection) => IteratorWorks just like entries(), but iterates in the reverse order.
keyArray() => (collection) => Array<key>Returns an array of keys on the value. Immutable.js has no function that does this, they have keys() which returns an iterator, and keySeq() which returns an Immutable.js Seq.
valueArray() => (collection) => Array<any>Returns an array of values on the collection. Immutable.js has no function that does this, they have values() which returns an iterator, and valueSeq() which returns an Immutable.js Seq.
entryArray() => (collection) => Array<[key, value]>Returns an array of entries (e.g. [key, value] tuples) of the value. Immutable.js has no function that does this, they have entries() which returns an iterator, and entrySeq() which returns an Immutable.js Seq.
butLast() => (collection) => newCollectionReturns a new collection of the same type containing all entries except the last.
isEmpty() => (collection) => booleanReturns true when the collection is empty, such as an empty object or an array with no elements.
isNotEmpty() => (collection) => booleanReturns true when the collection is not empty.
count() => (collection) => numberReturns the number of keys on the collection.
size() => (collection) => numberReturns the number of keys on the collection. Immutable.js has this as a getter on their collections, Unmutable.js offers this as a function.
startsWith(otherCollection) => (collection) => booleanReturns true when the collection starts with otherCollection. The elements of otherCollection are compared deeply with those in collection.
endsWith(otherCollection) => (collection) => booleanReturns true when the collection starts with otherCollection. The elements of otherCollection are compared deeply with those in collection.
clone() => (collection) => newCollectionReturns a clone of collection if collection is an array or object, or returns the collection unchanged if given an Immutable.js Map or List. Immutable.js data types are inherently immutable so do not need to be explicitly cloned.
replaceEqual(otherCollection) => (collection) => newCollectionIf otherCollection is deeply equal to collection, otherCollection is returned, or else collection is returned.
This can be useful if you have a data source that always recreates a data structure, such as JSON.parse(), but you want to avoid needlessly passing new instances of unchanged objects and arrays downstream.
replaceEqualDeep(otherCollection) => (collection) => newCollectionIf otherCollection is deeply equal to collection, otherCollection is returned. If not, each of collections children are compared against otherCollection, and if they are deeply equal then that part of otherCollection is inserted into the result. This process continues recursively down the data structure.
This can be useful if you have a data source that always recreates a data structure, such as JSON.parse(), but you want to avoid needlessly passing new instances of unchanged objects and arrays downstream.
log(message: string = '', type: string = 'log') => (value) => valueReturns an evaluator that passes the value through unchanged, but also calls console[type](message, value). Useful for debugging.
compose(...functions: Array<Function>) => (value) => newValueComposes (combines) functions together from right to left. Returns a function that's ready to accept a value and run it through all the functions in the pipe.
composeWith(value, ...functions: Array<Function>) => (value) => newValueAccepts an value as the first argument, and composes (combines) functions in the remaining arguments together from right to left.
pipe(...functions: Array<Function>) => (value) => newValueComposes (combines) functions together from left to right. Returns a function that's ready to accept a value and run it through all the functions in the pipe.
pipeIf(condition: (value) => boolean, ...functions: Array<Function>) => (value) => newValueLike pipe(), but the first argument is a conditional function that is passed the value. If a truthy value is returned from the conditional function, all functions in the pipe are executed. If a falsey value is returned, then the remaining functions in the pipe are skipped.
pipeWith(value, ...functions: Array<Function>) => (value) => newValueAccepts an value as the first argument, and composes (combines) functions in the remaining arguments together from left to right.
identity() => (value) => valueA function that passes values through with no change. Useful for readable code.
isAssociative(value: any) => booleanWorks like Immutable.js isAssociative() but also identifies plain Javascript arrays and objects as being associative.
isCollection(value: any) => booleanWorks like Immutable.js isCollection() but also identifies plain Javascript arrays and objects as being collections.
isImmutable(value: any) => booleanReturns true if value is an Immutable.js data type, or false otherwise.
isIndexed(value: any) => booleanWorks like Immutable.js isIndexed() but also identifies plain Javascript arrays as being indexed.
isKeyed(value: any) => booleanWorks like Immutable.js isKeyed() but also identifies plain Javascript objects as being keyed.
isObject(value: any) => booleanTests if something extends from object and is not primitive, which includes arrays, functions, class instances and all Immutable.js types, and does not include undefined, null, string, number, and boolean.
isOrdered(value: any) => booleanWorks like Immutable.js isOrdered() but also identifies plain Javascript arrays as being ordered.
isPlainObject(value: any) => booleanTests if the value is a plain object according to is-plain-object.
isRecord(value: any) => booleanisValueObject(value: any) => booleanAn alias for isObject() to align with Immutable.js naming convention.
isWriteable(value: any) => booleanTests if a data type can be used with unmutable functions that write or modify data. Returns true for any Immutable.js types, array and plain objects.
A helper function that allows you to turn any method into a point-free function.
overload({[arity: string]: Function})Simulates function overloading in Javascript. Pass it an object with functions as values. The objects keys should be strings of numbers that indicate how many arguments each function expects to receive.
doIf(
predicate: (value) => boolean,
ifTrue: (value) => newValue,
ifFalse: (value) => newValue = ii => ii
) => (value) => newValuePasses the value to the predicate function. If the predicate returns true, the value is then passed through the ifTrue function and the result is returned. If the predicate returns false then the value is simply returned unchanged.
If the third argument ifFalse is provided, then the value will be passed through ifFalse when the predicate returns false.
range([start = 0], end, [step = 1])Helper function to generate an array of sequential numbers. Simply a re-export of lodash.range
Immutable.js has a wonderfully symmetrical and well thought out API, and it deals with data immutably. Its a fantastic library, but its drawbacks are that it's large in size and impossible to take only the parts of the API you want, which makes it quite unsuitable for small Javascript libraries. It also requires you use its special data types which can involve a lot of to-ing and fro-ing between normal Javascript types and Immutable.js types.
Ramdajs and Lodash/fp both allow for functions to be composed together point-free style, which is a really nice way of programming, and it naturally keeps the bundle size small as you only import what you need. Lodash/fp's main drawback is that its API is large, unwieldy and unpredictable. Ramdajs' API is better planned, but it's overall pretty esoteric and alien to programmers who are not used to fully functional languages. And both of those libraries use currying, which unmutable deliberately avoids*.
Unmutable deliberately avoids currying because currying in Javascript requires functions to have fixed arity, and Unmutable cannot have fixed arity functions because it must match Immutable.js which uses optional arguments / variable arity functions.Unmutable is written and maintained by Damien Clarke, with feedback from others at 92green. All online library discussion happens over on Github.
I hope this library helps solve some Javascript data manipulation problems for you. 🎉