Search code examples
typescripttypescript-genericstyping

Type definition for comparing (and updating) certain fields of objects in Typescript


I'd like to compare and assign certain fields of two objects, let's say:

const left = { a: 1, ignored: 5, something: 7 };
const right = { a: 2, ignored: 6, else: 8, id: 18 };

And I want to call leftToRight(left, right, ['a']) after which right should be:

{ a: 1, ignored: 6, id: 18 }

and I want to call some other function and pass right.id, which I know does exists on the second argument.

My current approach is:

leftToRight(left, right, keys) {
  let changed = false;
  
  for (const key of keys) {
    if (!Object.is(left[key], right[key])) {
      right[key] = left[key];
      changed = true;
    }
  }

  doSomething(right.id)

  return changed
}

I'm struggling to find the appropriate type definition :-(

Initial approach:

leftToRight<T>(left: T, right: T, keys: Array<keyof T>): boolean

leads to: "Property 'id' does not exist on type 'T'" and I found no way to check this ('id' in right)

Second attempt:

leftToRight<T>(left: T, right: T & {id: number}, keys: Array<keyof T>): boolean

leads to "Type 'T' is not assignable to type '{ id: number; }'" for right[key] = left[key]

Third attempt:

leftToRight<T, U extends {id: number}>(left: T, right: U, keys: Array<keyof T & keyof U>): boolean

again leads to an error for the assignment right[key] = left[key] because the types T and U could be completely unrelated.


Solution

  • Edit:

    You have clarified your requirements to say that Left and Right might each have properties which are not present in the other. To handle this scenario, we need two generic types.

    Right is an object which includes an id. We want the selected keys to be present in both Left and Right and we want them to have the same value types. I defined the generic Keys as any subset of the keys of Right. I defined Left as the subset of Right containing all these keys.

    function leftToRight<Right extends {id: number}, Keys extends keyof Right>(
      left: Pick<Right, Keys>, right: Right, keys: Keys[]
    ) {
      let changed = false;
      
      for (const key of keys) {
        if (!Object.is(left[key], right[key])) {
          right[key] = left[key];
          changed = true;
        }
      }
    
      doSomething(right.id)
    
      return changed
    }
    

    Playground Link

    Original Answer:

    I tried a few things before finding one that works. Of course this is not the cleanest definition. I got errors when using the generic to describe Left and adding id to get Right, but removing from Right works.

    We say that Right is an object with an {id: number} property and Left must have all properties of Right except for id. In order for the elements of keys to be present on both objects, we need to ignore the key id from keyof Right.

    function leftToRight<Right extends {id: number}>(
      left: Omit<Right, 'id'>, right: Right, keys: (Exclude<keyof Right, 'id'>)[]
    ) {
      let changed = false;
      
      for (const key of keys) {
        if (!Object.is(left[key], right[key])) {
          right[key] = left[key];
          changed = true;
        }
      }
    
      doSomething(right.id)
    
      return changed
    }
    

    Playground Link