Typescript: How can I make entries in an ES6 Map based on an object key/value type

Here’s the closest I can imagine getting, although I still don’t understand why we don’t just use plain objects to begin with:

type ObjectToEntries<O extends object> = { [K in keyof O]: [K, O[K]] }[keyof O]

interface ObjectMap<O extends object> {
  forEach(callbackfn: <K extends keyof O>(
    value: O[K], key: K, map: ObjectMap<O>
  ) => void, thisArg?: any): void;
  get<K extends keyof O>(key: K): O[K];
  set<K extends keyof O>(key: K, value: O[K]): this;
  readonly size: number;
  [Symbol.iterator](): IterableIterator<ObjectToEntries<O>>;
  entries(): IterableIterator<ObjectToEntries<O>>;
  keys(): IterableIterator<keyof O>;
  values(): IterableIterator<O[keyof O]>;
  readonly [Symbol.toStringTag]: string;
}

interface ObjectMapConstructor {
  new <E extends Array<[K, any]>, K extends keyof any>(
    entries: E
  ): ObjectMap<{ [P in E[0][0]]: Extract<E[number], [P, any]>[1] }>;
  new <T>(): ObjectMap<Partial<T>>;
  readonly prototype: ObjectMap<any>;
}

const ObjectMap = Map as ObjectMapConstructor;

The idea is to make a new interface, ObjectMap, which is specifically dependent on an object type O to determine its key/value relationship. And then you can say that the Map constructor can act as an ObjectMap constructor. I also removed any methods that can change which keys are actually present (and the has() method is redundantly true also).

I can go through the trouble of explaining each method and property definition, but it’s a lot of type-juggling. In short you want to use K extends keyof O and O[K] to represent the types normally represented by K and V in Map<K, V>.

The constructor is a bit more annoying in that type inference doesn’t work the way you’d like, so guaranteeing type safety comes in two steps:

// let the compiler infer the type returned by the constructor
const myMapInferredType = new ObjectMap([
  ['key1', 'v'], 
  ['key2', 1],  
]);

// make sure it's assignable to `ObjectMap<Values>`: 
const myMap: ObjectMap<Values> = myMapInferredType;

If your myMapInferredType doesn’t match ObjectMap<Values> (e.g., you are missing keys or have the wrong value types) then myMap will give you errors.

Now you can use myMap as an ObjectMap<Values>, similarly to how you’d use a Map instance, with get() and set(), and it should be type safe.

Please note again… this seems like a lot of work for a more complex object with trickier typings and no more functionality than a plain object. I would seriously warn anyone using a Map whose keys are subtypes of keyof any (that is, string | number | symbol) to strongly consider using a plain object instead, and be sure that your use case really necessitates a Map.

Playground link to code

Leave a Comment