4

Lets say I have an existing object obj1 = {a: 'A', b: 'B', c: 'C', d: 'D'}

I have another object something like obj2 = {b: 'B2', c: 'C2', d: 'D2', e: 'E2'}

Now I want to selectively copy the values of only a few properties from obj2 to obj1, for rest of the properties in obj1, I want them unchanged.

Now, say I want to copy of the values of properties b and d. I'd want to do something like

obj1 = {a: 'A', b: 'B', c: 'C', d: 'D'};
obj2 = {b: 'B2', c: 'C2', d: 'D2', e: 'E2'};

copyPropertyvalues(
    obj2, // source
    obj1, // destination
    [obj2.b, obj2.d] // properties to copy >> see clarification below
); // should result in obj1 = {a: 'A', b: 'B2', c: 'C', d: 'D2'}

How do I write this method? Note that I also do not want to provide the list of properties as strings, as I'd want compile time safety as much as possible.

So basic asks here are:

  1. Copy only a few property values from an object
  2. Copy to an existing object & not create a new one
  3. Retain other property values in destination object
  4. Avoid strings for property names (like 'b', 'd') and leverage TypeScript safety as much as possible

Clarification based on comment:

When I say obj2.b or so in (pseudo) code example

  • I do not mean literally obj2.b
  • Rather some way to check at compile time that b is actually a property on obj2's type
Arghya C
  • 9,805
  • 2
  • 47
  • 66
  • You are already using typescript, so there is no reason to not use strings, and type them as e.g. `keyof obj2`. – ASDFGerte Nov 16 '21 at 17:02
  • My intention here is to NOT provide property names as `'b', 'd'` as that'd have much higher chance of errors – Arghya C Nov 16 '21 at 17:03
  • 2
    You say "I also do not want to provide the list of properties as strings, as I'd want compile time safety as much as possible" but this is the opposite of the truth, you can use string literal types like `"b"` to pick out the keys you want, but `obj2.b` is just some value and there's no way to know that it should go into the `b` property of the destination. It's as if I wanted to move a textbook from Alice's locker to her classroom, so I just hand you the book and don't say the name "Alice" at all. How do you know where it goes? It's much better if I say "Alice" and let you get the book. – jcalz Nov 16 '21 at 17:13
  • 2
    You can do [this](https://tsplay.dev/wQe5Zw), for example, and it works just fine. I don't even know how you'd begin to implement it with `[obj2.b, obj2.d]` though. If you want this as an answer I can write it up, as long as you understand that string keys are really the only reasonable solution and that it is definitely type safe. – jcalz Nov 16 '21 at 17:16
  • @jcalz Ah, sorry for the confusion. I didn't mean literally mean `obj2.b` or so, rather what I wanted to say is => something with bit more compile time safety to make sure `"b"` is actually a property of `obj2` – Arghya C Nov 16 '21 at 17:52
  • Updated the question, hopefully it's bit more clearer now – Arghya C Nov 16 '21 at 17:57
  • @jcalz your code sample does work for me, please add as an answer – Arghya C Nov 16 '21 at 18:16
  • 1
    @jcalz `I don't even know how you'd begin to implement it with [obj2.b, obj2.d]` In C# it's a language feature, for TS there's a plugin: https://github.com/dsherret/ts-nameof. `[nameof(obj2.b), nameof(obj2.d)]` would be converted at compile time to `["b", "d"]`. Pro: when refactoring these properties the IDE doesn't miss the strings. Con: It's verbose and you need a plugin. – Thomas Nov 16 '21 at 19:16
  • @Thomas I also had some similar thoughts like the C# `nameof()`, but do not want to include a plugin yet (the plugin seems good though). I'd want the compiler time safety, but @jcalz 's solution works for me. – Arghya C Nov 17 '21 at 06:39

1 Answers1

3

My suggestion for copyPropertyvalues() would be something like this:

function copyPropertyvalues<T, K extends keyof T>(s: Pick<T, K>, d: T, ks: K[]) {
    ks.forEach(k => d[k] = s[k]);
    return d;
}

The function is generic in the type T of the destination object, and the type K of the key names you're going to copy from the source to the destination object. I assume that you want to require that the properties you're copying from the source should be the same type as the properties already in the destination; for example, you wouldn't copy the "a" property from a source of type {a: number} to a destination of type {a: string}, since you'd be writing a number to a string and violating the type of the destination.

Note that we don't need the source object to be exactly T; it only has to agree with T at all the keys in K. That is, we only say that it must be Pick<T, K> using the Pick<T, K> utility type.

And note that we are definitely passing in an array of key strings, which the compiler will infer not just as string, but specifically as a union of string literal types which are a subset of keyof T (using the keyof type operator to get a union of the known keys of T). So if you pass in ["b", "d"], the compiler will infer that as being of type Array<"b" | "d"> and not just Array<string>. This will give you the type safety you're looking for, without trying to worry about something like nameof which does not exist in JavaScript (and will not exist in TypeScript until it does, see microsoft/TypeScript#1579).


Okay, let's try it out:

copyPropertyvalues(
    obj2, // source
    obj1, // destination
    ["b", "d"] // properties to copy
);
console.log(obj1)
/* {
  "a": "A",
  "b": "B2",
  "c": "C",
  "d": "D2"
}  */

Looks like it behaves how you want. And if you put the wrong keys in there you get compile time errors:

copyPropertyvalues(
    obj2, // error!
    obj1,
    ["a"]
)

copyPropertyvalues(
    obj2,// error!
    obj1,
    ["z"]
)

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360