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