GitHub

objectDiff()

Find differences between two objects with optional depth control and detailed change tracking.

Utility Function
Objects
Comparison
Syntax
objectDiff(obj1, obj2, options)
Parameters

obj1
Object

The first object to compare (original).

obj2
Object

The second object to compare (modified).

options
object
optional

Configuration options.

  • depth - Maximum depth to compare (default: Infinity)
  • includeEqual - Include unchanged values (default: false)
  • arrayDiff - How to handle array differences (default: 'replace')
  • ignoreKeys - Array of keys to ignore during comparison
Returns

Object
- Object containing the differences with change metadata.

Examples

Basic Object Comparison

const original = {
  name: "John",
  age: 30,
  city: "New York"
};

const modified = {
  name: "John",
  age: 31,
  city: "Boston"
};

const diff = objectDiff(original, modified);
console.log(diff);
// {
//   age: { from: 30, to: 31 },
//   city: { from: "New York", to: "Boston" }
// }

Nested Object Differences

const obj1 = {
  user: {
    profile: {
      name: "Alice",
      settings: {
        theme: "dark",
        notifications: true
      }
    },
    posts: 5
  }
};

const obj2 = {
  user: {
    profile: {
      name: "Alice",
      settings: {
        theme: "light",
        notifications: true,
        language: "en"
      }
    },
    posts: 7
  }
};

const diff = objectDiff(obj1, obj2);
console.log(diff);
// {
//   user: {
//     profile: {
//       settings: {
//         theme: { from: "dark", to: "light" },
//         language: { from: undefined, to: "en" }
//       }
//     },
//     posts: { from: 5, to: 7 }
//   }
// }

Array Differences

const obj1 = {
  tags: ["javascript", "react"],
  scores: [85, 90, 78]
};

const obj2 = {
  tags: ["javascript", "react", "nodejs"],
  scores: [85, 92, 78, 88]
};

// Default array handling (replace)
const diff1 = objectDiff(obj1, obj2);
console.log(diff1);
// {
//   tags: { 
//     from: ["javascript", "react"], 
//     to: ["javascript", "react", "nodejs"] 
//   },
//   scores: { 
//     from: [85, 90, 78], 
//     to: [85, 92, 78, 88] 
//   }
// }

// Detailed array diff
const diff2 = objectDiff(obj1, obj2, { arrayDiff: 'detailed' });
console.log(diff2);
// {
//   tags: {
//     added: [{ index: 2, value: "nodejs" }],
//     removed: [],
//     modified: []
//   },
//   scores: {
//     added: [{ index: 3, value: 88 }],
//     removed: [],
//     modified: [{ index: 1, from: 90, to: 92 }]
//   }
// }

Depth Control

const deepObj1 = {
  level1: {
    level2: {
      level3: {
        level4: {
          value: "deep"
        }
      }
    }
  }
};

const deepObj2 = {
  level1: {
    level2: {
      level3: {
        level4: {
          value: "deeper"
        }
      }
    }
  }
};

// Limit comparison depth
const shallowDiff = objectDiff(deepObj1, deepObj2, { depth: 2 });
console.log(shallowDiff);
// {
//   level1: {
//     level2: { 
//       from: { level3: { level4: { value: "deep" } } },
//       to: { level3: { level4: { value: "deeper" } } }
//     }
//   }
// }

// Full depth comparison
const fullDiff = objectDiff(deepObj1, deepObj2);
console.log(fullDiff);
// {
//   level1: {
//     level2: {
//       level3: {
//         level4: {
//           value: { from: "deep", to: "deeper" }
//         }
//       }
//     }
//   }
// }

Ignoring Specific Keys

const config1 = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
  timestamp: "2023-01-01T00:00:00Z",
  sessionId: "abc123"
};

const config2 = {
  apiUrl: "https://api.example.com",
  timeout: 8000,
  retries: 5,
  timestamp: "2023-01-02T00:00:00Z",
  sessionId: "def456"
};

// Ignore timestamp and sessionId (they always change)
const diff = objectDiff(config1, config2, {
  ignoreKeys: ['timestamp', 'sessionId']
});

console.log(diff);
// {
//   timeout: { from: 5000, to: 8000 },
//   retries: { from: 3, to: 5 }
// }

Including Equal Values

const obj1 = { a: 1, b: 2, c: 3 };
const obj2 = { a: 1, b: 4, c: 3 };

// Default: only differences
const diffOnly = objectDiff(obj1, obj2);
console.log(diffOnly);
// { b: { from: 2, to: 4 } }

// Include equal values
const fullComparison = objectDiff(obj1, obj2, { includeEqual: true });
console.log(fullComparison);
// {
//   a: { from: 1, to: 1, equal: true },
//   b: { from: 2, to: 4, equal: false },
//   c: { from: 3, to: 3, equal: true }
// }

Configuration Management

// Track configuration changes
class ConfigManager {
  constructor() {
    this.currentConfig = {};
    this.history = [];
  }

  updateConfig(newConfig) {
    const diff = objectDiff(this.currentConfig, newConfig, {
      ignoreKeys: ['lastModified', 'version']
    });

    if (Object.keys(diff).length > 0) {
      this.history.push({
        timestamp: new Date().toISOString(),
        changes: diff,
        previousConfig: { ...this.currentConfig }
      });

      this.currentConfig = { ...newConfig };
      console.log('Configuration updated:', diff);
    } else {
      console.log('No configuration changes detected');
    }
  }

  getChangeHistory() {
    return this.history;
  }

  rollback(steps = 1) {
    if (this.history.length >= steps) {
      const targetHistory = this.history[this.history.length - steps];
      this.currentConfig = { ...targetHistory.previousConfig };
      this.history = this.history.slice(0, -steps);
      console.log(`Rolled back ${steps} step(s)`);
    }
  }
}

// Usage
const configManager = new ConfigManager();

configManager.updateConfig({
  database: { host: 'localhost', port: 5432 },
  cache: { ttl: 3600 }
});

configManager.updateConfig({
  database: { host: 'prod-db.com', port: 5432 },
  cache: { ttl: 7200 },
  logging: { level: 'info' }
});

console.log(configManager.getChangeHistory());

Form State Tracking

// Track form changes for dirty state detection
class FormTracker {
  constructor(initialData) {
    this.initialData = { ...initialData };
    this.currentData = { ...initialData };
  }

  updateField(fieldName, value) {
    this.currentData[fieldName] = value;
  }

  getChanges() {
    return objectDiff(this.initialData, this.currentData);
  }

  isDirty() {
    const changes = this.getChanges();
    return Object.keys(changes).length > 0;
  }

  getChangedFields() {
    const changes = this.getChanges();
    return Object.keys(changes);
  }

  reset() {
    this.currentData = { ...this.initialData };
  }

  save() {
    const changes = this.getChanges();
    if (Object.keys(changes).length > 0) {
      console.log('Saving changes:', changes);
      this.initialData = { ...this.currentData };
      return changes;
    }
    return null;
  }
}

// Usage
const formTracker = new FormTracker({
  name: 'John Doe',
  email: 'john@example.com',
  preferences: {
    theme: 'dark',
    notifications: true
  }
});

formTracker.updateField('name', 'Jane Doe');
formTracker.updateField('preferences', {
  theme: 'light',
  notifications: true
});

console.log('Is dirty:', formTracker.isDirty()); // true
console.log('Changed fields:', formTracker.getChangedFields()); // ['name', 'preferences']
console.log('Changes:', formTracker.getChanges());
// {
//   name: { from: 'John Doe', to: 'Jane Doe' },
//   preferences: {
//     theme: { from: 'dark', to: 'light' }
//   }
// }