Search code examples
typescripttypescript-generics

Typescript: Generic function for creating a lookup object


I currently have a function to which I can pass an array of objects, where the objects all have the _id key. From that object the function creates a lookup object, which has the _id fields as keys and the corresponding objects as values.

function createLookupById<S extends string, T extends object & { _id: S }>(source: Array<T>) {
    return Object.fromEntries(source.map((el) => [el._id, el]));
}

/*
Example input: [
  { _id: "foo", otherKey: "value" }, { _id: "bar", yetAnotherKey: "abc" }
]

Output: {
  "foo": { _id: "foo", otherKey: "value" },
  "bar": { _id: "bar", yetAnotherKey: "abc" }
}
*/

How can I make this function even more generic, so that I can specify the key which it shall use for creating the lookup (currently fixed to _id) while still keeping the type checking?


Solution

  • You could add a parameter, keyFn: (v: V) => string -

    function createLookup<V>(
      source: Array<V>,
      keyFn: (t: V) => string
    ): { [k: string]: V; } {
      return Object.fromEntries(source.map((el) => [keyFn(el), el]));
    }
    
    console.log(createLookup(
      [
        { _id: "foo", otherKey: "value" },
        { _id: "bar", yetAnotherKey: "abc" },
      ],
      el => el._id,
    ))
    
    {
      "foo": {
        "_id": "foo",
        "otherKey": "value"
      },
      "bar": {
        "_id": "bar",
        "yetAnotherKey": "abc"
      }
    }
    

    View it on typescript playground.


    One limitation you are experiencing is Object.fromEntries will only create an object where the keys can be string -

    interface ObjectConstructor {
        /**
         * Returns an object created by key-value entries for properties and methods
         * @param entries An iterable object that contains key-value entries for properties and methods.
         */
        fromEntries<T = any>(
          entries: Iterable<readonly [PropertyKey, T]>
        ): { [k: string]: T; }; // ⚠️ k: string
    
        ...
    

    I might suggest you use a more suitable lookup type, like Map -

    function createLookup<K,V>(source: Array<V>, keyFn: (t: V) => K): Map<K,V> {
      return new Map(source.map((el) => [keyFn(el), el]))
    }
    

    Now the _id could be a number or any other type -

    const data = [
      { _id: 123, otherKey: "value" },
      { _id: 456, yetAnotherKey: "abc" },
    ]
    
    const numDict = createLookup(
      data,
      el => el._id,
    )
    
    console.log(numDict)
    
    Map (2) {
      123 => {
        "_id": 123,
        "otherKey": "value"
      },
      456 => {
        "_id": 456,
        "yetAnotherKey": "abc"
      }
    } 
    
    console.log(numDict.get(123))
    
    {
      "_id": 123,
      "otherKey": "value"
    }