ts-object-difference
v1.0.2
Published
A flexible, handler-based object difference detection utility for TypeScript/Node.js.
Readme
ts-object-difference
A flexible, handler-based object difference detection utility for TypeScript/Node.js projects.
Installation
npm install ts-object-differenceFeatures
- Handler Architecture: Easily add custom comparison logic for any data type.
- Date Support: Built-in support for native
Date,Moment.js, andFirestore Timestamps. - Heuristic Timestamps: Detects Unix seconds vs milliseconds automatically.
- Cleaned Outputs: Automatically filters out ignored keys from results.
Basic Usage
import { getObjectDifferences } from "ts-object-difference";
const oldData = { name: "John", age: 30 };
const newData = { name: "John", age: 31 };
const diff = getObjectDifferences(oldData, newData);
// Result: { before: { age: 30 }, after: { age: 31 } }Options
The getObjectDifferences function accepts an optional options object:
ignoreKeys(string[]): Keys to skip during comparison (e.g.,['updatedAt']).arrayCompareKeys({ arrayName: string, key: string }[]): List of keys to use for matching items in specific arrays.arrayFullReturn(boolean): If true, returns the entire array if any item changed (instead of deltas).maxDepth(number): Protection against circular references (default: 10).disableDefaultHandlers(boolean): If true, standard handlers are disabled.
Advanced Usage
Keyed Array Comparison
By default, the plugin compares arrays by content (e.g., if any element changes, it reports a change). For complex entities with unique IDs, you should define arrayCompareKeys so the plugin can perform a "smart delta" (identifying exactly which item was added, removed, or modified).
import { getObjectDifferences } from "ts-object-difference";
const oldData = {
tasks: [{ id: 1, name: "Setup", status: "done" }],
};
const newData = {
tasks: [
{ id: 1, name: "Setup", status: "archived" },
{ id: 2, name: "Deploy", status: "pending" },
],
};
const diff = getObjectDifferences(oldData, newData, {
// Map specific array field names to their unique ID property
arrayCompareKeys: [{ arrayName: "tasks", key: "id" }],
});
/*
Result shows ONLY the changes for Task 1 and the addition of Task 2:
{
before: {
tasks: [ { status: 'done' }, null ]
},
after: {
tasks: [ { status: 'archived' }, { id: 2, name: 'Deploy', status: 'pending' } ]
}
}
*/Date Support
The plugin is "Date-Aware" and correctly compares disparate date types (Native Date, Moment.js, Firestore Timestamp, and isoString). It ignores order/type differences and compares the underlying Unix timestamp. It is also "Price-Safe" (it won't misinterpret a number like 1700 as a Unix date unless it's compared against an actual Date object).
Handling Ghost Diffs (Filtered Objects)
If an object only contains changes in ignoreKeys, the plugin will return null instead of an empty object, keeping your diffs clean.
The engine uses a "First-Match" strategy. It iterates through a registry of handlers and uses the first one where canHandle(...) returns true. Custom handlers are checked before default handlers, allowing you to override any behavior.
Selective Handler Use
You can use disableDefaultHandlers in combination with the exported defaultHandlers or DefaultHandlerName enum to pick exactly what you want:
import {
getObjectDifferences,
defaultHandlers,
DefaultHandlerName
} from 'ts-object-difference';
// Only use the Date and Object handlers, skipping Array and Primitive logic
const filteredHandlers = defaultHandlers.filter(h =>
h.name === DefaultHandlerName.Date || h.name === DefaultHandlerName.Object
);
const diff = getObjectDifferences(old, new, { disableDefaultHandlers: true }, filteredHandlers);Creating a Handler
A handler must implement the DiffHandler interface:
interface DiffHandler {
name: string;
// logic to determine if this handler should process the current values
canHandle: (
oldV: any,
newV: any,
key: string,
options: DiffOptions,
) => boolean;
// logic to perform the comparison
compare: (
oldV: any,
newV: any,
key: string,
options: DiffOptions,
recurse: DiffRecurse,
) => DiffResult | null;
}Example: Custom ID-based Comparison
If you want a specific property metadata to always be treated as a simple "Different/Not Different" string instead of a nested object:
import { getObjectDifferences, DiffHandler } from "ts-object-difference";
const MetadataOverride: DiffHandler = {
name: "metadataOverride",
canHandle: (o, n, key) => key === "metadata",
compare: (o, n) => {
const oldStr = JSON.stringify(o);
const newStr = JSON.stringify(n);
return oldStr !== newStr ? { before: o, after: n } : null;
},
};
const diff = getObjectDifferences(oldData, newData, {}, [MetadataOverride]);Internal Handlers (Reference)
The default registry contains:
DateHandler: Handles Native Dates, Moment, and Firestore Timestamps.KeyedArrayHandler: For arrays where you've defined a uniquekeyinarrayCompareKeys.StandardArrayHandler: For normal arrays (content-based diffing).ObjectHandler: Recursively compares nested objects.PrimitiveHandler: The "catch-all" for strings, numbers, and nulls.
