Search code examples
typescripttypescript-genericsobjectmapper

how to check if generic type mapper has key according to a string value


I'm trying to figure out how to use and work with generic type Mapper and a string value: I have a function called getItemsPerKey<K, B>(itemsMapper : Map<K,V[]>, distinctIdsList : number[]).The K type is either number or string. I would like to check if the K type value, which is either a string or a number is equal (or apply the has(key) extension function) to a specific string value.

This is the relevant code:

export class SomeService
{
    //currently K can be number or string
    getItemsPerKey<K, V>(itemsMapper : Map<K,V[]>, distinctIdsList : number[]) 
    {
        let filteredItemsMapper : Map<K, V[]> = new Map();
        for (let [key, itemsTypeV] of Object.entries(itemsMapper))  
        {
            if (distinctIdsList.includes(+key))
            {
                /*This is problematic - "Argument of type 'string' is not assignable to parameter of type 'K'.
  'K' could be instantiated with an arbitrary type which could be unrelated to 'string'."*/ 
                if (filteredItemsMapper.has(key))
                {
                    ....
                }
            }
        }
    }
}

Solution

  • The real issue here is that you are using Object.entries() on itemsMapper, which is not at all going to do what you want. You are confusing the keys/values held by the map as data, verses the keys/values on the itemsMapper object itself. You should instead be using Map.prototype.entries(). In other words:

    • DON'T write Object.entries(itemsMapper)
    • DO write itemsMapper.entries()

    To demonstrate, let's make a Map<string, number> to look at:

    const testMap = new Map([["a", 1], ["b", 2]]);
    

    It's got two entries in it. But if we use Object.entries(), nothing happens:

    for (let [key, val] of Object.entries(testMap)) {
      console.log(key, val) // nothing happens
    }
    
    console.log(Object.entries(testMap).length); // 0
    

    Hmm, it has no entries? I thought it had two. The problem is that Object.entries() is not iterating over the data held by the map; it's iterating over the map's own enumerable properties. Of which there are none. We could add some:

    const mapWithAdditionalProp = Object.assign(
      new Map([["a", 1], ["b", 2]]), 
      { someProperty: "hello" }
    );
    console.log(mapWithAdditionalProp.someProperty.toUpperCase()); // HELLO
    

    Now mapWithAdditionalProp is a map holding those two entries, but it also has its own property named someProperty. Now let's do Object.entries():

    for (let [key, val] of Object.entries(mapWithAdditionalProp)) {
      console.log(key, val) // "someProperty", "hello"
    }
    

    So that's great if we're trying to shove some other properties onto the map object itself. But terrible if you're trying to actually get at the data in the map.

    On the other hand, Map.prototype.entries() is exactly intended to iterate over the data held by the map:

    for (let [key, val] of testMap.entries()) {
      console.log(key, val) // "a", 1 ; "b", 2
    }
    

    So you should use itemsMapper.entries() instead of Object.entries(itemsMapper). When you do:

    class SomeService {
      //currently K can be number or string <-- then you should constrain it to be such
      getItemsPerKey<K extends string | number, V>(
        itemsMapper: Map<K, V[]>, 
        distinctIdsList: number[]
      ) {
        let filteredItemsMapper: Map<K, V[]> = new Map();
        for (let [key, itemsTypeV] of itemsMapper.entries()) {
          if (distinctIdsList.includes(+key)) {
            if (filteredItemsMapper.has(key)) {
            }
          }
        }
      }
    }
    

    That compiles with no error. Hooray! And even better, it will probably do what you want at runtime.


    Note that the string vs K issue was indirectly telling you the problem. Objects in JavaScript only really have string (or symbol) keys. And Object.entries() only gives you the string keys. So Object.entries(itemsMapper) is going to hand you an array of pairs where the first item in each pair is a string. You get Array<[string, any]>.

    But a Map<K, V> can have any type whatsoever as K; it can use strings or numbers or Dates as keys (although using objects as keys in a Map is often not what people want to do since they are compared by reference). When you call itemsMapper.entries() then, you get IterableIterator<[K, V[]]> (it's not an array but you can iterate it like one), and the first item in each pair is a K, as you want.

    So the fix isn't to force checking a string key with has(); it's to check the right sort of key instead.

    Playground link to code