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?

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"
}

You could add another generic parameter to specify your lookup key.

function createLookupById<
  K extends string,
  S extends string,
  T extends { [Key in K]: S },
>(key: K, source: T[]) {
  return Object.fromEntries(source.map((el) => [el[key], el]));
}

const result1 = createLookupById("_id", [
  { _id: "foo", otherKey: "value" },
  { _id: "bar", yetAnotherKey: "abc" },
]);

const result2 = createLookupById("customId", [
  { customId: "foo", otherKey: "value" },
  { customId: "bar", yetAnotherKey: "abc" },
]);

TypeScript Playground

Leave a Comment