Is it possible to extract Map keys as type in TypeScript?

Issue

In TypeScript you can do

const obj = {
  '123-123': 1,
  '456-456': 2,
}

type objKeys = keyof typeof obj

And objKeys is a union type '123-123' | '456-456'.

Is something similar possible if obj was not an object, but a Map?

const map = new Map<string, number>([
  ['123-123', 1],
  ['456-456', 2],
])

type mapKeys = ???

Solution

If you write new Map<string, number>(...) then you are explicitly giving the Map a string key type, which means it will allow you to set() and get() and string-valued key. If you want to constrain the keys to a particular union of string literal types then you will need to change how you construct the Map. For example, if you define MapKeys ahead of time, you can use it instead of string:

type MapKeys = '123-123' | '456-456';
const map = new Map<MapKeys, number>([
  ['123-123', 1],
  ['456-456', 2],
]);

But this is a bit redundant, and backwards from what you’re asking for anyway. You want the compiler to infer MapKeys from the arguments passed to the Map constructor.


Since you said that the use case is for the Map to be constant (so not only are its keys constrained, but its values also do not change), then you might be better served by the ReadonlyMap<K, V> interface provided by TypeScript. There is no ReadonlyMap at runtime, but the TypeScript compiler will allow you to assign a Map instance to a variable of type ReadonlyMap.

And if we want the compiler to infer MapKeys, we might want to constrain the constructor’s key type to string, which gives the compiler a hint that strings.

Both of these imply that a helper function to create new ReadonlyMap<K, V> where K is constrained to string will help:

function ReadonlyMapWithStringKeys<K extends string, V>(
  iterable: Iterable<[K, V]>): ReadonlyMap<K, V> {
  return new Map(iterable)
}

const map = ReadonlyMapWithStringKeys([
  ['123-123', 1],
  ['456-456', 2],
])
// const map: ReadonlyMap<"123-123" | "456-456", number>

Now we can see that map is of a type whose keys are strongly typed. We can now define MapKeys from it. There are multiple ways to do it; one is using conditional type inference with the infer keyword

type MapKeys = typeof map extends ReadonlyMap<infer K, any> ? K : never;
// type MapKeys = "123-123" | "456-456"

Or you could use utility types like the Parameters<T> type to ask the compiler what the first parameter of map.get() is:

type MapKeys = Parameters<typeof map["get"]>[0]
// type MapKeys = "123-123" | "456-456"

Playground link to code

Answered By – jcalz

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published