75

Is there any way to have a TypeScript enum compatible with strings from JSON?

For example:

enum Type { NEW, OLD }

interface Thing { type: Type }

let thing:Thing = JSON.parse('{"type": "NEW"}');

alert(thing.type == Type.NEW); // false

I would like thing.type == Type.NEW to be true. Or more specifically, I wish I could specify the enum values to be defined as strings, not numbers.

I am aware that I can use thing.type.toString() == Type[Type.NEW] but this is cumbersome and seems to make the enum type annotation confusing and misleading, which defeats its purpose. The JSON is technically not supplying a valid enum value, so I shouldn't type the property to the enum.

So what I am currently doing instead is using a string type with static constants:

const Type = { NEW: "NEW", OLD: "OLD" }

interface Thing { type: string }

let thing:Thing = JSON.parse('{"type": "NEW"}');

alert(thing.type == Type.NEW); // true

This gets me the usage I want, but the type annotation string is way too broad and error prone.

I'm a bit surprised that a superset of JavaScript doesn't have string based enums. Am I missing something? Is there a different way this can be done?


Update TS 1.8

Using string literal types is another alternative (thanks @basaret), but to get the desired enum-like usage (above) it requires defining your values twice: once in a string literal type, and once as a value (constant or namespace):

type Type = "NEW" | "OLD";
const Type = {
    NEW: "NEW" as Type,
    OLD: "OLD" as Type
}

interface Thing { type: Type }

let thing:Thing = JSON.parse(`{"type": "NEW"}`);

alert(thing.type === Type.NEW); // true

This works but takes a lot of boilerplate, enough that I don't use it most of the time. For now I'm hoping the proposal for string enums will eventually make the roadmap.


Update TS 2.1

The new keyof type lookup allows for the string literal type to be generated from the keys of a const or namespace, which makes the definition a little less redundant:

namespace Type {
    export const OLD = "OLD";
    export const NEW = "NEW";
}
type Type = keyof typeof Type;

interface Thing { type: Type }

const thing: Thing = JSON.parse('{"type": "NEW"}');
thing.type == Type.NEW // true

Update TS 2.4

TypeScript 2.4 added support for string enums! The above example becomes:

enum Type {
    OLD = "OLD",
    NEW = "NEW"
}

interface Thing { type: Type }
const thing: Thing = JSON.parse('{"type": "NEW"}');
alert(thing.type == Type.NEW) // true

This looks nearly perfect, but there's still some heartache:

  • You still have to write the value twice, ie OLD = "OLD", and there's no validation that you don't have a typo, like NEW = "MEW"... this has already bitten me in real code.
  • There's some oddities (perhaps bugs?) with how the enum is type checked, its not just a string literal type shorthand, which is what would be truly correct. Some issues I've bumped into:

    enum Color { RED = "RED", BLUE = "BLUE", GREEN = "GREEN" }
    
    type ColorMap = { [P in Color]: number; }
    
    declare const color: Color;
    declare const map: ColorMap;
    map[color] // Error: Element implicitly has an 'any' type because type 'ColorMap' has no index signature.
    
    const red: Color = "RED"; // Type '"RED"' is not assignable to type 'Color'.
    const blue: Color = "BLUE" as "RED" | "BLUE" | "GREEN"; // Error: Type '"RED" | "BLUE" | "GREEN"' is not assignable to type 'Color'.
    

    The equivalent code with enum Color replaced by string literal types work fine...

Yeah, I think I have OCD about this, I just want my perfect JS enums. :)

Aaron Beall
  • 49,769
  • 26
  • 85
  • 103

5 Answers5

33

If you are using Typescript before the 2.4 release, there is a way to achieve that with enums by casting the values of your enum to any.

An example of your first implementation

enum Type {
    NEW = <any>"NEW",
    OLD = <any>"OLD",
}

interface Thing { type: Type }

let thing:Thing = JSON.parse('{"type": "NEW"}');

alert(thing.type == Type.NEW); // true

Typescript 2.4 has built in support for string enums already, so the cast to any would be no longer necessary and you could achieve it without the use of String Literal Union Type, which is ok for validation and autocomplete, but not so good for readability and refactoring, depending on the usage scenario.

Felipe Sabino
  • 17,825
  • 6
  • 78
  • 112
  • Thanks! I wish I had known about the `any` assertion before. Now, I'm trying out TS 2.4 string enums, and it's quite close to what I originally wanted... but I've found some issues with the way TS type checks it... – Aaron Beall Nov 10 '17 at 21:55
  • @Aaron cool, glad to help! Also, you might want to check the [ts-enums](https://github.com/LMFinney/ts-enums) project as it makes enum handling very versatile and powerful for a lot of use cases – Felipe Sabino Nov 11 '17 at 13:21
  • 3
    Wow, this turned out to be a lifesaver! As I [commented](/q/17380845/how-to-convert-string-to-enum-in-typescript#comment94130934_52933064) on another answer, this only works if your keys match the string values as no reverse mapping is generated for string enums. Whatsoever, casting the string values to `any` is enough for the compiler to create a reverse mapping. It would be really helpful, if you could edit this info into your answer. – altocumulus Dec 05 '18 at 15:50
  • 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
  • Good solution. However, I recommend using variable names unrelated to the concept in your example. Your `type: Type` is going to confuse newbies. Same thing with an `enum` named `Type`. Use clearly tangential names like "foo" and "bar" or at least "MyEnum". – user358041 Mar 16 '23 at 17:26
5

In case someone's still looking at this question in 2021:

@Aaron wrote in the original question:

This looks nearly perfect, but there's still some heartache:

You still have to [...]

enum Color { RED = "RED", BLUE = "BLUE", GREEN = "GREEN" }

type ColorMap = { [P in Color]: number; }

declare const color: Color;
declare const map: ColorMap;
map[color] // Error: Element implicitly has an 'any' type because type 'ColorMap' has no index signature.
// [...]

The equivalent code with enum Color replaced by string literal types work fine...

Yeah, I think I have OCD about this, I just want my perfect JS enums. :)

1. keyof typeof enumObj

Regarding,

The equivalent code with enum Color replaced by string literal types work fine...

use the typeof and keyof operators in chained conjunction.

type ColorKeys = keyof typeof Color
type ColorMap = { [P in ColorKeys]: number; } // will have strongly typed keys

No more implicit any when accessing map: ColorMap.
This will work with numeric enums as well (which can (and should more often than not) be const).

From Typescript Handbook - Enums at compile time:

Even though Enums are real objects that exist at runtime, the keyof keyword works differently than you might expect for typical objects. Instead, use keyof typeof to get a Type that represents all Enum keys as strings.

2. ts-enum-util

Check out ts-enum-util, which offers strongly typed interfaces to (likely) all your enum-related needs.

Johannes Pille
  • 4,073
  • 4
  • 26
  • 27
3

TS 2.9.2
My solution:

export enum Enums { VALUE1, VALUE2 }

and when I have value from API json:

 switch (response.enumValue.toString()) { //can be without toString if we have string value from JSON.
    case Enums[Enums.VALUE1]:
      ...
    case Enums[Enums.VALUE2]:
      ...
 }
  • I mentioned this in my OP, it's not what I'm going for. Basically it means your JSON is *not* satisfying valid enum values and you need some kind of interop code (I guess what your example is showing). I want `response.enumValue == Enums.VALUE1` to be true and type compatible. – Aaron Beall Sep 06 '19 at 15:54
1

but the type annotation string is way too broad and error prone.

Agreed. One quick workaround (if you have the luxury of code generation you can automate this):

interface Thing { type: "NEW" | "OLD" }

These are called string literals in a union. More : https://basarat.gitbooks.io/typescript/content/docs/tips/stringEnums.html

basarat
  • 261,912
  • 58
  • 460
  • 511
  • This looks promising. We will investigate when we upgrade to TS 1.8. I was playing with it and wasn't able to work out how to use the string literal values as constants, ie something like `thing.type == Type.NEW`. – Aaron Beall Mar 02 '16 at 23:22
  • 1
    [This is as close as I could get](http://www.typescriptlang.org/Playground#src=type%20Type%20%3D%20%22NEW%22%20%7C%20%22OLD%22%3B%0Aconst%20Type%20%3D%20%7B%0A%09NEW%3A%20%22NEW%22%2C%0A%09OLD%3A%20%22OLD%22%0A%7D%0A%0Ainterface%20Thing%20%7B%20type%3A%20Type%20%7D%0A%0Alet%20thing%3AThing%20%3D%20JSON.parse(%60%7B%22type%22%3A%20%22NEW%22%7D%60)%3B%0A%0Aalert(thing.type%20%3D%3D%3D%20Type.NEW)%3B), but it requires defining the `Type` and the values twice, once as a `type` for use as an interface type annotation, and again as a `const` for uses where I need the values. Can this be improved? – Aaron Beall Mar 02 '16 at 23:35
1

I've been using converter functions as a stopgap. Hopefully this thread comes to a resolution: https://github.com/Microsoft/TypeScript/issues/1206

enum ErrorCode {
    Foo,
    Bar
}

interface Error {
    code: ErrorCode;
    message?: string;
}

function convertToError(obj: any): Error {
    let typed: Error = obj as Error;

    // Fix any enums
    typed.code = ErrorCode[typed.code.toString()];
    return typed;
}
Ryan Burbidge
  • 192
  • 3
  • 13