Home > database >  How to perform a type-safe object map in Typescript?
How to perform a type-safe object map in Typescript?

Time:11-24

I'm trying to write a function in Typescript that maps an object, while guaranteeing that it keeps the same keys. I've tried various approaches to this, but haven't found a way that works:

function mapObject1<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  return Object.fromEntries(
    Object.entries(object)
      .map(([key, value]): [K, B] => [key, mapper(value)]),
  ); // error!
  // Type '{ [k: string]: B; }' is not assignable to type '{ [P in K]: B; }'
}

export function mapObject2<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  const result: { [P in K]?: B } = {};

  (Object.keys(object) as K[]).forEach((key: K) => {
    result[key] = mapper(object[key]);
  });

  return result; // error!
  // Type '{ [P in K]?: B | undefined; }' is not assignable to type '{ [P in K]: B; }'
}

In mapObject1, using Object.entries() and Object.fromEntries() causes the type of the keys to be converted to string. In mapObject2, result's keys have to be optional since it starts out empty, causing Typescript to not be able to recognize that all of the same keys as in object are present. How should I approach solving this issue?

CodePudding user response:

Unfortunately the TypeScript compiler is unable to verify that the implementation is safe for a few reasons, and the most pragmatic way forward is to take special care that your implementation is written properly and then use type assertions to just tell the compiler that the values have the types you claim they have:

function mapObject1<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  return Object.fromEntries(
    Object.entries(object)
      .map(([key, value]) => [key, mapper(value as A)]),
    // assert --------------------------------> ^^^^^
  ) as { [P in K]: B }
  //^^^^^^^^^^^^^^^^^^ <-- assert
}

function mapObject2<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  const result: { [P in K]?: B } = {};

  (Object.keys(object) as K[]).forEach((key: K) => {
    // assert -------> ^^^^^^ (you already did this)
    result[key] = mapper(object[key]);
  });

  return result as { [P in K]: B };
  // assert --> ^^^^^^^^^^^^^^^^^^
}

That all compiles fine.


The reasons the compiler is unable to follow the logic:

  • Related