Handling Atomic Built-ins
Relevant source files
The following files were used as context for generating this wiki page:
Handling Atomic Built-ins refers to the mechanism by which @b2m9/keyfold distinguishes between "plain" objects that should be deep-merged and "opaque" or "atomic" objects that should be replaced wholesale. This system ensures that complex JavaScript built-ins, such as Date, Map, or Set, as well as user-defined class instances, are treated as single values rather than containers to be recursed into.
The project enforces this behavior both at runtime—by identifying non-plain objects and swapping their references—and at the type level, by providing a mechanism to mark specific types as Atomic in the Delta<T> type definition. This prevents the merger from attempting to traverse internal structures that are not intended for structural merging.
Sources: README.md:120-142, src/objects.ts:7-15, src/types.ts:25-36
Runtime Atomic Identification
At runtime, the merger uses the isPlainObject utility to determine if a value should be merged or replaced. A "plain object" is defined as a non-null object whose prototype is either Object.prototype or null. Any object failing this check is considered atomic and is swapped wholesale by reference.
Replacement Logic
When the fold logic encounters a delta value that is not a plain object, it bypasses the recursive foldObject process and returns the delta value directly as a replacement for the base value.
The diagram shows the decision path taken by the fold function when processing values.
Sources: src/merge.ts:40-66, src/objects.ts:11-15
Type-Level Atomic Definitions
The Delta<T, Atomic> type uses a conditional check to determine if a property should admit a partial update or require a full value replacement. If a type is identified as Atomic or a "Known Opaque" built-in, the Delta type resolves to the type itself rather than a recursive partial.
Known Opaque Types
The library pre-defines several built-in JavaScript types as opaque. These are structures that the runtime fold never enters.
| Type | Description |
|---|---|
Date | Date objects are treated as scalars. |
RegExp | Regular expressions are replaced rather than merged. |
Map / Set | Standard collections are considered atomic. |
Promise | Asynchronous placeholders are replaced wholesale. |
ArrayBuffer | Binary data buffers are treated as opaque values. |
Error | Built-in Error instances are matched by their structural surface. |
Sources: src/types.ts:50-63, src/types.ts:65-74
The Atomic Type Parameter
Users can provide a custom Atomic type parameter to createMerger to handle application-specific classes. This is crucial because TypeScript's structural typing might otherwise treat a class instance as a plain object if it only contains public data fields.
// Example from README
class Money {
readonly #nominal = true; // Private field ensures identity
constructor(readonly cents: number) {}
}
const mergeMoney = createMerger<State, Money>();
// Delta requires a complete Money instance, not { cents: number }
Sources: README.md:120-136, src/types.ts:25-36
Implementation Details
Structural Sharing and Identity
Atomic replacements are governed by the principle of structural sharing. If a replacement delta value is the exact same reference as the base value (evaluated via Object.is), the merger returns the original base reference to maintain idempotency.
Sources: src/merge.ts:98-101, AGENTS.md:21-22
Built-in Error Handling
Errors are handled with specific care because they are structurally unique. The library includes a utility type IsBuiltinError<T> that checks if a type matches the Error interface exactly. If it matches, it is treated as atomic. If it is a plain object acting as an error payload (without the Error prototype), it remains mergeable.
Sources: src/types.ts:67-74, test/types.ts:80-92
Summary of Atomic Handling
The handling of atomic built-ins provides a safety boundary, preventing the library from corrupting internal states of complex objects. By treating everything except plain objects and arrays as atomic, keyfold maintains its "JSON-like" focus while remaining compatible with standard JavaScript runtimes.
Sources: README.md:143-150, AGENTS.md:24-27