Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I would like to use Map instead of object map to declare some keys and values. But Typescript doesn't seem to support index types for ES6 Map, is that correct and are there any workarounds?

Additionally, I would like to make the values type-safe as well so that each entry in the map has the correct type for the value corresponding to the key.

Here is some pseudo-code that describes what I am trying to achieve:

type Keys = 'key1' | 'key2';

type  Values = {
  'key1': string;
  'key2': number;
}

/** Should display missing entry error */
const myMap = new Map<K in Keys, Values[K]>([
  ['key1', 'error missing key'],
]);

/** Should display wrong value type error for 'key2' */
const myMap = new Map<K in Keys, Values[K]>([
  ['key1', 'okay'],
  ['key2', 'error: this value should be number'],
]);

/** Should pass */
const myMap = new Map<K in Keys, Values[K]>([
  ['key1', 'all good'],
  ['key2', 42],
]);

Edit: more code that partially describes my use case

enum Types = {
  ADD = 'ADD',
  REMOVE = 'REMOVE',
};

/** I would like type-safety and autocompletion for the payload parameter */
const handleAdd = (state, payload) => ({...state, payload});

/** I would like to ensure that all types declared in Types are implemented */
export const reducers = new Map([
  [Types.ADD, handleAdd],
  [Types.REMOVE, handleRemove]
]);
like image 967
Nikolai Hegelstad Avatar asked Feb 27 '19 13:02

Nikolai Hegelstad


People also ask

How do I map a key-value pair in TypeScript?

To define a Map with array values in TypeScript, type the map to have keys of a specific type and set the values to have an array type, e.g. const map1 = new Map<number, string[]>() . All of the key-value pairs in the Map must conform to the specified type. Copied!

Can an object be a key in a JavaScript map?

Introduction to JavaScript Map object A key of an object must be a string or a symbol, you cannot use an object as a key. An object does not have a property that represents the size of the map.

How do you get a Keys map in TypeScript?

We use the get() method of a map in TypeScript to get the value for a key in a map. A map is a key-value pair data structure.


1 Answers

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

like image 99
jcalz Avatar answered Oct 06 '22 00:10

jcalz