evolve-ts
Version:
Immutably update nested objects with patches containing values or functions to update values
86 lines (82 loc) • 3.91 kB
JavaScript
;
class Unset {
} // using class so the type will be unique
/** sentinel value that will cause its key to be removed */
const unset = Unset;
const toString = {}.toString;
const isObj = (o) => toString.call(o) === "[object Object]";
const isFn = (x) => x != unset && typeof x === "function"; // this will return false positives for classes other than unset
const curry = (fn) => (...args) => args.length < fn.length
? (...moreArgs) => curry(fn)(...args, ...moreArgs)
: fn(...args);
const baseEvolve = (patch, target) => {
if (patch && isObj(patch)) {
// shave bytes by reassigning, but not mutating, arguments
target = isObj(target) ? { ...target } : {};
Object.keys(patch).forEach((key) => {
target[key] = baseEvolve(patch[key], target[key]);
if (target[key] == unset)
delete target[key];
});
return target;
}
else if (isFn(patch)) {
return patch(target);
}
else {
return patch;
}
};
/** deeply merges changes from a patch object into a target object, the patch object can contain new values or functions to update values */
const evolve = curry(baseEvolve);
/** polymorphic type alias for evolve: deeply merges changes from a patch object into a target object, the patch object can contain new values or functions to update values */
const evolve_ = evolve;
const baseShallowEvolve = (patch, target) => {
// shave bytes by reassigning, but not mutating, arguments
target = { ...target };
Object.keys(patch).forEach((key) => {
const update = patch[key];
target[key] = isFn(update) ? update(target[key]) : update;
if (target[key] == unset)
delete target[key];
});
return target;
};
/** merges changes from a patch object into a target object, the patch object can contain new values or functions to update values */
const shallowEvolve = curry(baseShallowEvolve);
const createAdjust = (ev) => (predicateOrIndex, updaterOrPatch, array) => {
// shave bytes by reassigning, but not mutating, arguments
if (!isFn(updaterOrPatch)) {
updaterOrPatch = ev(updaterOrPatch);
}
// track if any item was changed
let changed;
const updater = (item) => ((changed = 1), updaterOrPatch(item));
if (predicateOrIndex < 0) {
// allow using negative indexes as offsets from end
predicateOrIndex = array.length + predicateOrIndex;
}
const mapped = array.map(isFn(predicateOrIndex)
? (item) => (predicateOrIndex(item) ? updater(item) : item)
: (item, i) => (i === predicateOrIndex ? updater(item) : item));
return changed ? mapped : array;
};
/** conditionally maps values in an array with a callback function or patch. Value(s) to map can be specified with an index or predicate function. Negative indexes are treated as offsets from the array length */
const adjust = curry(createAdjust(evolve));
/** conditionally maps values in an array with a callback function or patch. Value(s) to map can be specified with an index or predicate function. Negative indexes are treated as offsets from the array length */
const shallowAdjust = curry(createAdjust(shallowEvolve));
const createMap = (ev) => (updaterOrPatch, array) => array.map(isFn(updaterOrPatch)
? (item) => updaterOrPatch(item) // clamp args to 1
: ev(updaterOrPatch));
/** maps values in an array with a callback function or patch */
const map = curry(createMap(evolve));
/** maps values in an array with a callback function or patch */
const shallowMap = curry(createMap(shallowEvolve));
exports.adjust = adjust;
exports.evolve = evolve;
exports.evolve_ = evolve_;
exports.map = map;
exports.shallowAdjust = shallowAdjust;
exports.shallowEvolve = shallowEvolve;
exports.shallowMap = shallowMap;
exports.unset = unset;