Explicit Field Deletions
Relevant source files
The following files were used as context for generating this wiki page:
Explicit Field Deletions in keyfold provide a deterministic mechanism for removing properties from nested state trees during a merge operation. Unlike standard JSON Merge Patch, which often uses null for deletions, keyfold treats null as valid data and requires the use of specific sentinels or tokens to trigger the removal of a field or a keyed list item.
This system distinguishes between three distinct field states during a merge: "unmentioned" (ignored), "null" (kept as data), and "deleted" (removed from the resulting tree). This precision ensures that structural sharing is preserved and that the base state is never mutated accidentally.
Deletion Mechanisms
The library provides three primary methods for performing deletions, each scoped to a specific context: object fields, JSON-serialized "wire" deltas, and keyed-list items.
Object Field Deletion (DELETE)
The internal DELETE sentinel is a unique symbol used within in-memory JavaScript/TypeScript deltas to remove an optional field from an object.
The diagram above illustrates the logic inside foldObject where the DELETE symbol triggers a field removal if the key exists in the base object.
Sources: src/merge.ts:90-100, src/sentinels.ts:2
Wire Deletion (DELETE_TOKEN)
For JSON transports where symbols cannot be preserved, keyfold offers a "Wire Deletes" protocol. When enabled via configuration, the string @@keyfold/delete is interpreted as a field deletion command.
| Sentinel | Type | Context | Description |
|---|---|---|---|
DELETE | symbol | In-memory | Removes optional fields in JS/TS environments. |
DELETE_TOKEN | string | JSON/Wire | Interpreted as deletion when wireDeletes is true. |
$delete | boolean | Keyed List | Removes an entire item from a keyed list. |
Sources: src/sentinels.ts:1-5, README.md:143-155
Keyed Item Tombstones ($delete)
Inside keyed lists, an item is deleted by providing its identity field along with a $delete: true property. This is referred to as a "tombstone".
Sources: src/reconcile.ts:125-144, README.md:152-154
Configuration and the Wire Protocol
The interpretation of the DELETE_TOKEN is optional and must be explicitly enabled during the creation of a merger.
The sequence diagram shows how the wireDeletes option influences the merging logic for string tokens.
Sources: src/merge.ts:23-32, src/paths.ts:28-30, test/wire.test.ts:20-30
Security and Type Safety
- Static Protection: The
Delta<T>type preventsDELETEfrom being used on required fields to maintain the integrity of the state shape. However, TypeScript cannot statically exclude theDELETE_TOKENstring literal from a generalstringtype, making wire validation a caller responsibility. - Reserved Keys: When
wireDeletesis active, theDELETE_TOKENcannot be used as an identity field in keyed lists. - Escaping: Inside a
replacepath or an unkeyed array, bothDELETEandDELETE_TOKENare treated as inert data and are not interpreted as operators.
Sources: src/types.ts:46-51, test/wire.test.ts:47-52, README.md:213-220
Implementation Logic
The deletion logic is embedded within the recursive fold and reconcile functions.
Deleting from Objects
In src/merge.ts, the foldObject function iterates through delta keys. If a value matches the deletion criteria, it lazily copies the base object (to preserve immutability) and removes the key.
// src/merge.ts:96-102
if (value === DELETE || (context.wireDeletes && value === DELETE_TOKEN)) {
if (hasBase) {
next ??= copyPlainObject(base) as Record<PropertyKey, unknown>;
delete next[key];
}
continue;
}
Sources: src/merge.ts:96-102
Deleting from Keyed Lists
The reconcile function in src/reconcile.ts manages item deletions. It first indexes the delta to identify tombstones. During the iteration of the base list, if a matching ID is marked as a tombstone, it is skipped in the resulting array.
// src/reconcile.ts:50-53
if (patch.tombstone) {
changed = true;
continue;
}
A tombstone must strictly contain only the identity field and $delete: true. Any additional fields or a different value for $delete will trigger a KeyfoldMergeError.
Sources: src/reconcile.ts:50-53, src/reconcile.ts:125-144
Summary of Semantics
Explicit field deletions in keyfold ensure that the removal of data is an intentional act. By providing specific symbols and tokens, the system avoids the ambiguity of null and undefined found in other merging strategies. Deleting a missing field is treated as a no-op, returning the original base reference and maintaining optimal performance through structural sharing.
Sources: README.md:135-142, AGENTS.md:29-33