43

I wanted to use string enums in typescript but I can't see a support for reversed mapping in it. I have an enum like this:

enum Mode {
    Silent = "Silent",
    Normal = "Normal",
    Deleted = "Deleted"
}

and I need to use it like this:

let modeStr: string;
let mode: Mode = Mode[modeStr];

and yes I don't know what is it there in modeStr string and I need it parsed to the enum or a fail at parsing in runtime if the string is not presented in the enum definition. How can I do that as neat as it can be? thanks in advance

Kamyar Ghajar
  • 431
  • 1
  • 4
  • 8
  • 1
    Possible duplicate of [Create an enum with string values in Typescript](https://stackoverflow.com/questions/15490560/create-an-enum-with-string-values-in-typescript) – ponury-kostek Jul 03 '17 at 10:41
  • 8
    @ponury-kostek Technically, that question does not address the issue of making a reverse mapping. Even if it does, it probably sits in one of the many answers hidden at the bottom, making the solution too hard to find. I say if there isn't a better duplicate, let's keep this one. – E_net4 Jul 03 '17 at 17:05
  • It's not a duplicate! – gyozo kudor Aug 17 '22 at 15:26

7 Answers7

30

We can make the Mode to be a type and a value at the same type.

type Mode = string;
let Mode = {
    Silent: "Silent",
    Normal: "Normal",
    Deleted: "Deleted"
}

let modeStr: string = "Silent";
let mode: Mode;

mode = Mode[modeStr]; // Silent
mode = Mode.Normal; // Normal
mode = "Deleted"; // Deleted
mode = Mode["unknown"]; // undefined
mode = "invalid"; // "invalid"

A more strict version:

type Mode = "Silent" | "Normal" | "Deleted";
const Mode = {
    get Silent(): Mode { return "Silent"; },
    get Normal(): Mode { return "Normal"; },
    get Deleted(): Mode { return "Deleted"; }
}

let modeStr: string = "Silent";
let mode: Mode;

mode = Mode[modeStr]; // Silent
mode = Mode.Normal; // Normal
mode = "Deleted"; // Deleted
mode = Mode["unknown"]; // undefined
//mode = "invalid"; // Error

String Enum as this answer:

enum Mode {
    Silent = <any>"Silent",
    Normal = <any>"Normal",
    Deleted = <any>"Deleted"
}

let modeStr: string = "Silent";
let mode: Mode;

mode = Mode[modeStr]; // Silent
mode = Mode.Normal; // Normal
//mode = "Deleted"; // Error
mode = Mode["unknown"]; // undefined
Rodris
  • 2,603
  • 1
  • 17
  • 25
  • 2
    Thanks...It could've been implemented in the typescript though. – Kamyar Ghajar Jul 12 '17 at 08:27
  • 3
    tslint will throw an error on that Enum example: *Element implicitly has an 'any' type because index expression is not of type 'number'*. I guess the issue is that in TS string Enums can not be reverse-mapped, see the comment in the String-Enum example at https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-4.html - This seems to be true for TS 2.4 where String-Enum was introduced but I also get the error in TS 2.6.2 – omni Dec 29 '17 at 08:45
  • 1
    @masi Since 2.4, TS has support for string enums, so you don't need to add the `` cast. – Rodris Dec 29 '17 at 13:09
  • 1
    @RodrigoPedrosa I'm aware of that. The error occues with and without the cast. There's a GIT issue open related to that problem (https://github.com/Microsoft/TypeScript/issues/9234) but it was closed as "working as intended". Seems like TS devs never used any other lang that supports String-Evals otherwise they wouldn't claim it works as intended. At best it works "as good as possible" for a lang that tries to bring types to an other lang (I have not checked the tech. reasons so far thought). – omni Dec 29 '17 at 13:10
  • @masi I am not sure about what you are saying. The reverse mapping works in the playground. Is your problem specifically with the tslint? – Rodris Dec 29 '17 at 13:23
  • @RodrigoPedrosa Yes, it's a tslint issue. I'm not sure if the resulting code is working in all cases (typesafe). Example: `enum Mode {Silent = "Silent"}; let mode: Mode = Mode["Silent"];` will produce the error I posted in tslint. – omni Dec 29 '17 at 13:28
  • We loose the compiler check in a switch when we cover all the cases though, and we must add a default case that throws (for instance when the compiler checks that all paths return a value), which in turn does not help by showing unhandled paths when we add new values. It's sad that TypeScript enums are so weird. – ofavre May 14 '20 at 18:02
  • It may show warning by your linter when use `"string value"` (consistent-type-assertions), the "workaround" is simply change to `"string value" as any` – Kai Aug 12 '22 at 09:57
7

The cleanest way I found so far is to make a secondary map:

let reverseMode = new Map<string, Mode>();
Object.keys(Mode).forEach((mode: Mode) => {
    const modeValue: string = Mode[mode as any];
    reverseMode.set(modeValue, mode);
});

Thus you can do let mode: Mode = reverseMode.get('Silent');

Advantages: no need to repeat the values, provides a way to enumerate the enum, keeps TSLint happy...

Edit: I originally write Mode[mode] but then TS might throw the error TS7015 on this line, so I added the cast.

Mike Lippert
  • 2,101
  • 1
  • 18
  • 12
PhiLho
  • 40,535
  • 6
  • 96
  • 134
4

If you're ok with using Proxies, I made a more generic & compact version:

stringEnum.ts

export type StringEnum<T extends string> = {[K in T]: K}
const proxy = new Proxy({}, {
  get(target, property) {
    return property;
  }
})
export default function stringEnum<T extends string>(): StringEnum<T> {
  return proxy as StringEnum<T>;
}

Usage:

import stringEnum from './stringEnum';
type Mode = "Silent" | "Normal" | "Deleted";
const Mode = stringEnum<Mode>();
forivall
  • 9,504
  • 2
  • 33
  • 58
2

This answer is from @PhiLho,

I dont have enough rep to comment on his message.

let reverseMode = new Map<string, Mode>();
Object.keys(Mode).forEach((mode: Mode) => {
    const modeValue: string = Mode[<any>mode];
    reverseMode.set(modeValue, mode);
});

However, the first argument for .set should be the key, and the second should be the value.

reverseMode.set(modeValue, mode);

should be...

reverseMode.set(mode, modeValue);

Resulting in this...

let reverseMode = new Map<string, ErrorMessage>();
Object.keys(ErrorMessage).forEach((mode: ErrorMessage) => {
    const modeValue: string = ErrorMessage[<any>mode];
    reverseMode.set(mode, modeValue);
});

The reason why @PhiLho might have missed this is because the keys and values for the original object provided by OP are the same.

1

None of the answers really worked for me, so I have to fall back to normal for the loop. my enum is

enum SOME_CONST{
      STRING1='value1', 
      STRING2 ='value2',
      .....and so on
}


getKeyFromValue =(value:string)=> Object.entries(SOME_CONST).filter((item)=>item[1]===value)[0][0];
Manjeet Singh
  • 4,382
  • 4
  • 26
  • 39
1

For all of you still struggling with this, this is a fast and dirty way of getting this done.

enum Mode {
    Silent = "Silent",
    Normal = "Normal",
    Deleted = "Deleted"
}

const currMode = 'Silent',
  modeKey = Object.keys(Mode)[(Object.values(Mode) as string[]).indexOf(currMode)];

However, there are some caveats, that you should be aware of!

The most likely reason Microsoft marked the Git bug as "working as intended", is probably due to the fact that this deals with String Literals where as enums were originally designed to be indexable. While the above code works and lints with TSLint, it has two issues. Objects are not guaranteed to retain the intended order when converted to an Array and we are dealing with String Literals which may or may not be unique.

Take a look at this StackBlitz where there are two Mode's with values of 'Silent'. You have zero guarantee that it won't return 'Ignore' over 'Silent' once the lookup completes.

https://stackblitz.com/edit/typescript-hyndj1?file=index.ts

Lionel Morrison
  • 566
  • 4
  • 15
1

The standard types achieves this function - just check for undefined and handle however.

type Mode = "Silent" | "Normal" | "Deleted";
const a = Mode["something else"];
//a will be undefined

Also, you can add on a record type to the above to extend the "enum", say to translate a short enum key into more verbose output (https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type):

const VerboseMode: Record<Mode, string> = {
  Silent: "Silent mode",
  Normal: "Normal mode",
  Deleted: "This thing has been deleted"
}
Luca
  • 403
  • 4
  • 2