Immutable Merge package
This package provides a relatively concise routine to handle merging multiple objects together with the following characteristics:
- No modifications will be made to any object
- Minimal updates. If only one value is updated three levels deep, only that value and the chain of containing objects will be recreated.
- Empty objects or undefined objects will be ignored and not cause a new branch to be created.
- Recursion is controllable in a variety of ways
Note that this does not provide a strict immutable package on its own. It also doesn't operate or return only readonly objects. This is to provide flexibility. It could easily be wrapped in various ways to provide that type of functionality, but it is provided in a more flexible form to be useful in other scenarios as well.
Deep Merge via
For standard deep merging, this package provides the
immutableMerge function. The signature is as follows:
This takes one or more objects of type
T and deep merges them. If objects are undefined or null in some manner they will be ignored. Merging via this routine (and all routines in the package) typically follow the semantics of
Object.assign, with a few extra behaviors.
- all values at a given level will overwrite, with the last writer winning
- if a key does not exist for an object it is ignored
- if a key does exist, even if it is
undefinedit will replace the previous value
- only non-array objects will recurse, arrays will be replaced rather than appended
- keys which exist and have a value of
undefinedwill be deleted
The peculiar pattern of deleting keys which end up as undefined is the only way to delete a key without violating the core principles. An example of key deletion might look like:
Custom Merging via
In many cases, merges have to follow additional rules to match the structure or behavior of objects passed in. This results in authoring custom merge routines to handle this constraint. This package allows for deep customization of merge behaviors via
Recursion options can be a boolean or a number with behavior interpreted as follows:
boolean- Should this recurse. If the value is
trueit will recurse infinitely, if
falseit will not recurse.
number- Recursion depth. A value of
0will not recurse any farther, a positive value will recurse that many additional levels before stopping, a negative value will recurse indefinitely.
This object allows very precise control of the recursion. At a given level it matches values by name of the key, or by the resulting type of the property.
Matching will happen in the following order:
- Merged object property key matches a key in MergeOptions.
- The type of the key is referenced in MergeOptions. Note that arrays (which are objects) are treated as being of type 'array' for this purpose.
The values within the options can have the following types:
||Behaves as if this value was passed into the recursive call. So 0/false mean merge but don't recurse for the matching child object, less than zero / true means recurse deeply, greater than zero means recurse that many times.|
||Run a function to handle the merge or invoke one of the built-in handlers for the library. See below for more information.|
||Forward the child
When merging values for a given key, providing a recursion handler allows custom processing. A handler function has the following signature:
The vals parameter will have collected all the non-undefined values from the input objects. Note that type checking is the responsibility of the handler function.
Built in handlers
Built-in handlers can be referenced by name. The currently supported built-in handlers are as follows:
||Append arrays rather than overwriting them.|
As an example, imagine props for react components, with a concept called SlotProps that has multiple props in the same object.
In this case style needs to be merged in a special manner and classNames need to be appended. Deep recursion is not desireable in the case where a prop might be an object as with partial values you might get unexpected behavior. Here are some examples of ways to make merge routines:
// all in one function// this could be broken into two parts, options for props// then options for slotProps that refer to the props object// then a wrapper for each
The ability to run a handler on something like a style as a part of merge is useful but in normal usage it has some limitations. If there is only one object in a branch or only one value of that type the handler won't run. If the processors are essential functions, or if it is desireable to run processors on a single object you can use
This convenience function runs the merge routine as a processor for one or more objects. An example use case might be to turn all style entries into a css class name if it is not already a css class name. This should have the following behavior:
- Style values two levels down should be processed
- The object should remain unchanged if nothing changed
- branches which are unchanged should be untouched
- If a style gets updated the object should be mimally mutated
The usage would be as follows. Given a processor called
;complexObject = processImmutable,complexObject;
While the primary use case is for a single object this allows merging to happen at the same time if so desired. Merging happens as normal with the exception that processors will still be called in the case where there is only one object.