1

I'm trying to compare two objects of the same type. What I want to achieve at the end, is a new object with only the properties that are different.

The solution implemented so far, with some previous help in another post, will, in fact, give me back an object with the properties that have changed, but, if the properties in inner objects are not in the same order, which happens since the ordering of object properties is non-standard in ECMAScript, I will also get those in the final object which is not what I want.

In the example below:

z returns

{
  "properties": {
    "shared": false,
    "query": "project!=\"JP\""
  }
} 

whilst I would want z to be:

{
  "properties": {
    "query": "project!=\"JP\""
  }
} 

since the only property different between the two objects is query

type X = {
        owner: {
            accountId: string,
            displayName: string
        },
        filter: {
            id: string,
            name: string,
        },
        properties: {
            id: string,
            query: string
            shared: boolean
            syncDate?: number
        }
    }


const a: X = {
    filter: {
        id: "10021",
        name: "fil"
    },
    owner: {
    accountId: "61498eeaa995ad0073bb8444",
    displayName: "Jorge Guerreiro"

    },
    properties: {
        id: "10021",
        query: 'project!="JP"',
        shared: false,
        syncDate: undefined
    }
}


const b: X = {
    filter: {
        id: "10021",
        name: "fil"
    },
    owner: {
    accountId: "61498eeaa995ad0073bb8444",
    displayName: "Jorge Guerreiro"

    },
    properties: {
        id: "10021",
        shared: false,
        query: 'project="JP"',
        syncDate: undefined
    }
}

const deepCompare = <T>(oldFilter: T, newFilter:T): Partial<T> => {
    return Object.values(oldFilter).reduce((bef, aft, i) => {
        const valueB = Object.values(newFilter)[i];
        const keyB = Object.keys(newFilter)[i];
        if (valueB instanceof Object && keyB) {
            const delta = deepCompare(valueB, aft);
            return Object.keys(delta).length > 0 ? { [keyB]: deepCompare(valueB, aft), ...bef } : bef;
        }
        return valueB !== aft && keyB ? { [keyB]: valueB, ...bef } : { ...bef };
    }, {});
}

const z = deepCompare<X>(a, b)

I'm a bit stuck on how to have the recursive function below do what I want. Any help would be great.

Thanks

Jorge Guerreiro
  • 682
  • 6
  • 22

2 Answers2

1

try this

// scope to return the end result
const deepCompare = <T>(oldFilter: T, newFilter:T): Partial<T> => {
    // resursive function where the target argument 
    // will be the filtered result of the current entry
    const traverse = (obj: any, filter: any, target: any = {}) => {
      // looping entries by key is the most performant way to
      // iterate through obj properties
      for (let k in obj)
        if (obj[k] instanceof Object && filter[k]) {
          // ad level, go deeper
          target[k] = {}
          let targetResult = traverse(obj[k], filter[k], target[k])
          
          // delete empty entries if so desired
          if (!Object.keys(targetResult).length) 
            delete target[k]
        }
        else if (obj[k] !== filter[k])
          target[k] = obj[k] // store value

        return target
    }

    return traverse(oldFilter, newFilter)
}

deepCompare(a,b)

for typing you need to make your objects indexable so that typescript doesnt complain, I usually do generic keyof typeof T[k] or infer the nested agruments

zergski
  • 793
  • 3
  • 10
  • 1
    question regarding the objects being indexable. in `traverse` the arguments are any, would it be good practice to use like `obj: T | keyof T`? – Jorge Guerreiro Aug 23 '22 at 12:17
  • yes, something like that.. sorry was too lazy to fully type it out.. cant remember exactly but something like this should work `traverse(obj: { [ k in keyof T ]?: T[k] })` that should make it indexable.. I'm on my phone so cant confirm – zergski Aug 23 '22 at 12:30
  • and you need to do the same with filter & target – zergski Aug 23 '22 at 12:31
  • 1
    I'll give it a go if not will comment again :), thank you for your prompt response – Jorge Guerreiro Aug 23 '22 at 12:41
  • 1
    Yea that didn't work. Used `const traverse = (obj: { [k in keyof T]: T[k] },`, but when calling `traverse(obj[k])` I get the error on `obj[k]` for `Argument of type 'T[Extract]' is not assignable to parameter of type 'object'. Type 'T[string]' is not assignable to type 'object'.` . It is assuming k is of that type – Jorge Guerreiro Aug 23 '22 at 12:57
  • oh I see, forgot that you need to assign the T to the obj so it can extend from it. here's an exact function definition I used some time ago that worked flawlessly `proxySet = (obj: T, handlers: { [ k in keyof T ]?: (v: T[ k ]) => void }, options: { alwaysNotify?: boolean } = {})` maybe that can help.. otherwise I'll take a closer look when I get home – zergski Aug 23 '22 at 13:07
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247483/discussion-between-jorge-guerreiro-and-zergski). – Jorge Guerreiro Aug 23 '22 at 13:44
  • 1
    That sounds great as I'm a bit stuck. Thank you very much :) – Jorge Guerreiro Aug 23 '22 at 14:15
  • 1
    The only solution I managed to go with was by casting `obj[k], filter[k], target[k]` `as unknown as object` which seems horrible – Jorge Guerreiro Aug 24 '22 at 09:29
  • 1
    id say it's about three words too many. did you know you can typecast with <>? e.g `Foo`, I wish id know that sooner – zergski Aug 24 '22 at 16:20
1

sorry got home pretty late, here's a typed version that works, posting as I've tested it

// pretty simple complicated as we're dealing with the same object type
type Accumulator<T> = { [k in keyof T]?: T[k] | {} }

// 1. just T =)
const deepCompare = <T extends object>(oldFilter: T, newFilter: T): Accumulator<T> => {

   // 3. here though a different generic for varying props
   const traverse = <O>(obj: O, filter: O, target: Accumulator<O> = {}): Accumulator<O> => {

     for (let k in obj)
       if (obj[k] instanceof Object && filter[k]) {

         target[k] = {}     // 4. O[k] for the next prop type
         let targetResult = traverse<O[typeof k]>(obj[k], filter[k], target[k])

         // delete empty entries if so desired
         if (!Object.keys(targetResult).length)
           delete target[k]
       }
       else if (obj[k] !== filter[k])
         target[k] = obj[k] // store value

       return target
   }

   // 2. still T on first recursion call
   return traverse<T>(oldFilter, newFilter)
}

deepCompare({a: {v:'fv', g: 'ghdg', h: 'kfn', k: {l: '2222' }}},{a: {v:'fv', g: '1111', h: 'kfn', k: {l: 'dfg' }}})

that is all

zergski
  • 793
  • 3
  • 10