9

I declare a map so I can convert the numeric enum value into the value our API expects.

export const StatusMap: ReadonlyMap<Status, string> = new Map([
    [Status.NEW, 'new'],
    [Status.PENDING, 'pending'],
]);

But when I do statusMap.get(Status.NEW) it always tells me that the return value is possibly undefined. Is there a way to force a map (or similar) to contain all enum values?

And yes I know you can technically do

export enum Status {
    NEW = 'new',
    PENDING = 'pending',
}

but let's be honest, this kind of ruins the point of enums (IMO).

Elias
  • 3,592
  • 2
  • 19
  • 42
  • If you can use regular object instead of `Map` it will be as simple as `Record`. But what's the point of mapping strings to numbers and vice versa if the api expects the strings? String enum doesn't ruin the point of enums – Aleksey L. May 29 '20 at 09:11
  • @AlekseyL. well, strings are way larger than numbers. It's not "enumerated" anymore. I could just do `type Status = 'new' | 'pending';` – Elias May 29 '20 at 09:18
  • Unless I misunderstood you, the core of your problem isn't really your enum, but the way [`ReadonlyMap` is defined](https://github.com/Microsoft/TypeScript/blob/v2.8.1/lib/lib.es6.d.ts#L4703-L4708). If you make a custom interface to match your needs you should have no worries. – Etheryte May 29 '20 at 09:18
  • @Nit well Sir then just tell me how to define it differently lol. And I don't understand why you linked me to the `ReadonlyMap` definition :D – Elias May 29 '20 at 09:20
  • @Elias this is called premature optimization And with `type Status = 'new' | 'pending';` if values are changed to `new1 | pending1` for example you'll need to update strings all over the place instead of updating the enum – Aleksey L. May 29 '20 at 09:22
  • @AlekseyL. Okay, you're right with the argument about the type. But why shouldn't I try to optimize this? If we're dealing with large datasets, let's say 500+ entries, those are a lot of strings. Especially because this is not the only enum we have. (We're doing calendar event stuff) For example, an event has multiple users, each of those users has a status specific to each event. I'm not the one to say if it should or shouldn't be optimized (still pretty new to programming), it just seemed logical. Would **you** say it is not necessary? – Elias May 29 '20 at 09:43
  • You should measure the impact.. Remember that you are still dealing with strings (when you get them from the api and they are already in memory) then you're adding numbers on top of that (hoping that strings will be garbage collected at some point) – Aleksey L. May 29 '20 at 09:47
  • 1
    @AlekseyL. Problem is that I'm implementing a cache + a local store lol. So they're not just in the API but also caches separately. But I will definitely consider your argument. :) thanks – Elias May 29 '20 at 09:50
  • @AlekseyL. wait... does that mean that strings are being passed by reference? Surely not?! Object yeah, but strings? really? When I modify them the original one doesn't change. – Elias May 29 '20 at 09:53
  • geeze someone could have told me that earlier... they are indeed passed by reference. Now I feel stupid. https://stackoverflow.com/a/1308668/10315665 – Elias May 29 '20 at 10:01
  • Actually I was talking about code, not the values at runtime – Aleksey L. May 29 '20 at 11:40
  • 1
    Anyway answering original question - you can define `type StatusMap = { get(status: T): string }` and use it for `statusMap`. Pay attention, it won't verify that all enum values are actually in map https://www.typescriptlang.org/play/index.html#code/KYOwrgtgBAygLgQzmAzlA3gKCjqA5AUQHUAabXABQLwBEBJPAcTIF9NM4BPAB2FkWQoAsgm5QAvBigBzYHAA8AFSjAAHnFAATNPCSoAfAAoUA1AC4oigJQWTAJwCWIaVDaYAxgHsQJqCb3CohJQIMAA7lAi3IYA2uQ4MbqCAHSEpFAA5KFhGQC6ZLhQiaYoyVS0DMyZvCCaTtJ5ZLlWUAg6JVEA3OxePnBQAGaensH+glHJsnCGSaipxFZAA – Aleksey L. May 29 '20 at 11:45
  • 1
    And here's version with object as map (it will verify all the enum members are listed in map) https://www.typescriptlang.org/play/index.html#code/KYOwrgtgBAygLgQzmAzlA3gKCjqA5AUQHUAabXABQLwBEBJPAcTIF9NMBjAexBTij5JUAWQQAHAFxQASsG4AnACYAeeEJQkBceQEsQAcwB8UALwZyOANprkKAHSEiAXSkByEMADursrijXEWzsqWgZGFyhXMVBFPX0fTBYAbnZuXn4AMy4uUy11UTEA9QdiJyA – Aleksey L. May 29 '20 at 11:48
  • @AlekseyL. Ah! `record`! If you write an "actual" answer I can accept it. But you helped me realize that strings are actually passed around by reference lol. That is really something that is not mentioned often. It definitely should be! – Elias May 29 '20 at 11:55

1 Answers1

15

If you can use regular object instead of Map, it can be defined as Record of enum members:

const statusMap: Record<Status, string> = {
    [Status.NEW]: 'new',
    [Status.PENDING]: 'pending',
};

Playground


Another option would be using type assertion that guarantees that all enum members are in the map:

type StatusMap = { get<T extends Status>(status: T): string }

const statusMap = new Map([
    [Status.NEW, 'new'],
    [Status.PENDING, 'pending'],
]) as StatusMap;

** Pay attention, with this approach it is not guaranteed that all the enum members are in the map at runtime, so if you choose to go this way - better to cover it by unit test.

Aleksey L.
  • 35,047
  • 10
  • 74
  • 84
  • In your first example, `Record`, is it appropriate/good form to get a value with `statusMap[]`? The reason I gravitated to a Map is to use `.get()`. – Joe Sadoski Jan 27 '22 at 17:25
  • @JoeSadoski could you elaborate on what's the issue with `statusMap[Status.NEW]`? – Aleksey L. Jan 28 '22 at 08:29
  • 1
    Sorry, please disregard that. I misunderstood how `Record` worked. I thought they worked like objects, where you could use dot notation OR bracket notation. Your TS Playground example explained their use better for me. Thank you! – Joe Sadoski Jan 29 '22 at 01:43