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) => newCollection
Returns 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) => newCollection
Returns 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) => newCollection
Returns a new collection which excludes the provided keys.
pick(keys: string[]|number[]) => (collection) => newCollection
Returns a new collection which excludes all keys that aren't listed in keys
.
clear() => (collection) => newCollection
Returns a new collection containing no keys or values.
push(...values: Array<any>) => (collection) => newCollection
Returns a new collection with the provided values
appended, starting at this collection's size
.
pop() => (collection) => newCollection
Returns a new collection with a size ones less than this collection, excluding the last index in this collection.
unshift(...values: Array<any>) => (collection) => newCollection
Returns a new collection with the provided values
prepended, shifting other values ahead to higher indices.
shift() => (collection) => newCollection
Returns 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) => newCollection
Moves 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) => newCollection
Returns 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) => newCollection
Passes 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) => newCollection
Returns 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) => newCollection
Returns 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) => newCollection
Changes the key of oldKey
to the key of newKey
swap(keyA: string|number, keyB: string|number) => (collection) => newCollection
Swaps the values at the given keys. Keys that don't exist are assumed to have a value of undefined
.
rotate(shift: number) => (collection) => newCollection
Rotates 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) => newCollection
Attempts to turn otherCollection
into collection
s data type, and returns newCollection
.
setMutate(key: string|number, value: any) => (collection) => alteredCollection
Like set()
but performs mutations on value
for objects and arrays. It is less expensive than set()
.
deleteMutate(key: string|number, value: any) => (collection) => alteredCollection
Like 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) => newCollection
Returns a new collection with values passed through a mapper function.
mapKeys(mapper: (key: string|number, value: any, original: any) => newKey) => (collection) => newCollection
Returns a new collection with keys passed through a mapper function.
mapEntries(mapper: ([key, value]: any, index: number, original: any) => [newKey, newValue]) => (collection) => newCollection
Returns a new collection with key value pairs passed through a mapper function.
flip() => (collection) => newCollection
Returns a new collection with its key values pairs flipped, so values become keys and keys become values
stableSortBy(comparatorValueMapper: value => valueToCompare, comparator?: Function) => (collection) => newCollection
Like 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) => newCollection
This 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) => newCollection
Returns 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) => newCollection
Returns 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) => newCollection
Returns 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} // Object
Iterates 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) => newCollection
Filters 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) => newCollection
Filters 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) => newCollection
Converts plain Javascript data types into arrays, and converts Immutable.js objects into List
s
toJSON() => (collection) => any
Turns 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) => newCollection
Converts plain Javascript data types into objects, and converts Immutable.js objects into Map
s.
toEntries() => (collection) => newCollection
Converts collections into arrays of key, value tuples.
pivot() => (collection) => newCollection
Pivots 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) => boolean
Returns true
if value
and otherValue
are not deeply equal, or false
otherwise.
strictEquals(otherCollection: any) => (collection: any) => number
Checks if collection
and otherCollection
are strictly equal. This complements equals()
, which checks for deep value equality.
shallowEquals(otherCollection: any) => (collection: any) => number
Checks 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) => Iterator
Works 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) => newCollection
Returns a new collection of the same type containing all entries except the last.
isEmpty() => (collection) => boolean
Returns true when the collection
is empty, such as an empty object or an array with no elements.
isNotEmpty() => (collection) => boolean
Returns true when the collection
is not empty.
count() => (collection) => number
Returns the number of keys on the collection.
size() => (collection) => number
Returns 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) => boolean
Returns true when the collection
starts with otherCollection
. The elements of otherCollection
are compared deeply with those in collection
.
endsWith(otherCollection) => (collection) => boolean
Returns true when the collection
starts with otherCollection
. The elements of otherCollection
are compared deeply with those in collection
.
clone() => (collection) => newCollection
Returns 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) => newCollection
If 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) => newCollection
If otherCollection
is deeply equal to collection
, otherCollection
is returned. If not, each of collection
s 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) => value
Returns an evaluator that passes the value through unchanged, but also calls console[type](message, value)
. Useful for debugging.
compose(...functions: Array<Function>) => (value) => newValue
Composes (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) => newValue
Accepts an value as the first argument, and composes (combines) functions in the remaining arguments together from right to left.
pipe(...functions: Array<Function>) => (value) => newValue
Composes (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) => newValue
Like 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) => newValue
Accepts an value as the first argument, and composes (combines) functions in the remaining arguments together from left to right.
identity() => (value) => value
A function that passes values through with no change. Useful for readable code.
isAssociative(value: any) => boolean
Works like Immutable.js isAssociative()
but also identifies plain Javascript arrays and objects as being associative.
isCollection(value: any) => boolean
Works like Immutable.js isCollection()
but also identifies plain Javascript arrays and objects as being collections.
isImmutable(value: any) => boolean
Returns true if value
is an Immutable.js data type, or false otherwise.
isIndexed(value: any) => boolean
Works like Immutable.js isIndexed()
but also identifies plain Javascript arrays as being indexed.
isKeyed(value: any) => boolean
Works like Immutable.js isKeyed()
but also identifies plain Javascript objects as being keyed.
isObject(value: any) => boolean
Tests 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) => boolean
Works like Immutable.js isOrdered()
but also identifies plain Javascript arrays as being ordered.
isPlainObject(value: any) => boolean
Tests if the value is a plain object according to is-plain-object.
isRecord(value: any) => boolean
isValueObject(value: any) => boolean
An alias for isObject()
to align with Immutable.js naming convention.
isWriteable(value: any) => boolean
Tests 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) => newValue
Passes 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. 🎉