0

I have the following situation with two a bit different objects:

const primary: PrimaryInterface = {
  numbers: [1,2,3,4], // <= 5
  other_numbers: [4,5,6,7], // <= 8
  //tons of other array fields to merge
}

const child: ChildInterface = {
  numbers: 5, //but be any number, even non unique
  other_numbers: 8
  //other fields, which have the same element stucture
}

And now I want to merge child values, up to primary object, as primary[key].push(child[key])

Sounds quite simple, and I wrote my own function which responsible for that kind of merge. But since I am refactoring my code as a typescript, I have the problem with accessing object values by key directly (via object[key], this question shows why: Element implicitly has an 'any' type because expression of type 'string' can't be used to index).

So instead of rewriting almost all of my code, I thought, probably Lodash (or any other familiar object/array manipulation library) with its methods could be useful. I have found the merge method in Lodash, can it be useful in my case?

AlexZeDim
  • 3,520
  • 2
  • 28
  • 64
  • 2
    I don't know why you need to use lodash, I assume your code is something like [this](https://tsplay.dev/wEVGvN) which can be made to compile with some [type assertions](https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions). I don't think any code that iterates over keys of an object without a string index signature can be guaranteed type safe by the compiler, so using type assertions to quiet the compiler's warnings might be the only way to proceed unless you are willing to use a hardcoded list of property names. Does the code I presented work for you? – jcalz Mar 08 '21 at 20:57
  • @jcalz, yeah, more than that. Actually, you gave me a bit of another point of view on this situation, since I am working with `for (const [key, value] of Object.entries(object))` in my own case. But to be honest, the real case is a bit complicated than that, since `primary` isn't a pure JS object. It's a `mongoose` document. (But let's don't think about that) ***And, I'd prefer to see your comment as an answer since it's truly very useful.*** – AlexZeDim Mar 08 '21 at 21:01

1 Answers1

1

Object types in TypeScript are open in the sense that if you have a value of type (say) {a: string, b: number}, while you can be sure that there will be a string property at key "a" and a number property at key "b", you cannot be sure that there will not be properties at keys other than "a" and "b". This openness allows for things like interface and class extensions, where sub-interfaces and sub-classes can have more properties than their parent interfaces and classes without violating the contract of the parent interface or class type. TypeScript does not have "exact types" (as requested in microsoft/TypeScript#12936) where you can say that Exact<{a: string, b: number}> is like {a: string, b: number} but is known to have no other properties but "a" and "b". So unless you add a string index signature to your types, the compiler will balk at the suggestion that the keys of an object will be limited to the ones it knows about. See Why doesn't Object.keys return a keyof type in TypeScript? for a possibly canonical answer here. The safe way to do this is to iterating over a hardcoded array of known keys, like ["numbers", "other_numbers", /* etc */].


But if you already had JavaScript code that was iterating over object properties with something like Object.keys() or Object.entries(), and it was working for you, then you were already running this risk in JavaScript to no apparent harm. If you'd like to continue doing this instead of rewriting your code, then you could always use type assertions to tell the compiler that while you appreciate its concern for the safety of your code, you are sure that it is safe enough for your purposes. For example:

function merge<T>(primary: { [K in keyof T]: Array<T[K]> }, child: T) {

  // the following type assertion is not always safe,
  // since primary may in fact have keys not known to the compiler
  // but we will assume that it doesn't
  const childKeys = Object.keys(child) as Array<keyof T>;

  childKeys.forEach(<K extends keyof T>(k: K) => primary[k].push(child[k]))

}

This implementation of merge() is a generic function that works the way you've described. The type assertion at childKeys is where you tell the compiler that you know that Object.keys(child) is an array of string, but you will treat it as an array of keyof T and face whatever consequences may come of being mistaken about that.

You could also do it this way instead:

function merge2<T>(primary: { [K in keyof T]: Array<T[K]> }, child: T) {
  for (const k in child) {
    primary[k].push(child[k]);
  }
}

Here the compiler actually makes the same (sometimes unjustified) assumption that you might make that for (const k in child) will iterate over only keys found in type T. And so you don't need any type assertions at all.


The important piece of this answer is that type assertions are okay. The compiler cannot always know what you know about the code. Sometimes the compiler is wrong, and a type assertion is useful to work around that. Sometimes it is technically correct but the issue it raises is unlikely to come up in your code and instead of rewriting your code, a type assertion can be used to just quiet the complaint. In either case, there will be times when you want the compiler to focus on other parts of your code instead of warning about something that is actually fine. At such times it makes sense for you to sit down and think hard about what you are doing to make sure that an assertion is warranted. If so, go for it!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360