import React from 'react'; type ChangedData = { key: keyof T; difference?: any; maxDepthReached?: boolean; newValue?: any; oldValue?: any; reactElementChanged?: boolean; }; type Changed = Array>; export type PropsDifference = { removed: Array; added: Array; changed: Changed; }; export type ShallowPropsDifference = { removed: Array; added: Array; changed: Array; }; export const getKeys = Object.keys as (obj: T) => Array; export const getKeysAddedRemovedCommon = (object1: T, object2: T) => { const oldKeys = object1 !== null ? getKeys(object1) : []; const newKeys = object2 !== null ? getKeys(object2) : []; const removed = oldKeys.filter((key) => !newKeys.includes(key)); const added = newKeys.filter((key) => !oldKeys.includes(key)); const common = oldKeys.filter((key) => newKeys.includes(key)); return { added, common, removed, }; }; export const serializeValue = (value: T[keyof T]) => { const valueType = typeof value; if (value === null) { return 'null'; } else if (value === undefined) { return 'undefined'; } else if (valueType === 'string' || valueType === 'number') { return value; } else if (valueType === 'symbol') { return ((value as unknown) as symbol).toString(); } // Calling toString of function returns whole function text with body. // So, just return function with name. else if (valueType === 'function') { return `function:${((value as unknown) as Function).name}`; } else if (valueType === 'object') { return { type: 'object', keys: Object.keys(value), }; } }; export const getPropsDifference = ( object1: T, object2: T, curDepth: number = 0, maxDepth: number = 2, keysToIgnore: Array = [], ): PropsDifference => { const { added, common, removed } = getKeysAddedRemovedCommon( object1, object2, ); const changed = [] as Changed; common.forEach((key) => { const value1 = object1[key]; const value2 = object2[key]; const value1Type = typeof value1; const value2Type = typeof value2; // Do comparision only if values doesn't match (or reference to same object in memory). // Or if key does not exist in keys to ignore. if (value1 !== value2 && keysToIgnore.indexOf(key) === -1) { // if both key value are objects and not referencing same object in memory. // then get recursive difference. if (React.isValidElement(value1) || React.isValidElement(value2)) { changed.push({ key, reactElementChanged: true, }); } else if (value1Type === 'object' && value2Type === 'object') { if (curDepth <= maxDepth) { const difference = getPropsDifference( value1, value2, curDepth + 1, maxDepth, ); changed.push({ key, difference, }); } else { changed.push({ key, maxDepthReached: true, }); } } else { changed.push({ key, oldValue: serializeValue(value1), newValue: serializeValue(value2), }); } } }); return { added, changed, removed, }; }; export const getShallowPropsDifference = ( object1: T, object2: T, ): ShallowPropsDifference => { const { added, common, removed } = getKeysAddedRemovedCommon( object1, object2, ); const changed = common.filter((key) => object1[key] !== object2[key]); return { added, changed, removed, }; };