0

TypeScript has added the erasableSyntaxOnly flag, that doesn't let you use Enums.

Have looked for several alternative options. Like:

My best solution yet, for type safety and code completion is object literals with type-definition:

export const Environment = {
   Development: 'development',
   Production: 'production',
   Test: 'test',
}
export type Environment =   typeof  Environment[keyof typeof Environment];

But, it's not pretty and looks hacky, especially the last line:

  1. Why do we need to add this convoluted line, just to get the type?
  2. Is Environment a type? A value? Both? This is not clear and error prone.

Is there a recommended way to migrate away from Enums? Haven't seen one in the docs

3
  • I don't see what makes your solution hacky. I would also make const Environment const. So } as const. The benefit of not using enums is that you wouldn't need explicitly import the enum everywhere where you need to compare the value, because you can 'just' use enter the value directly and it is statically checked. Commented Jul 3 at 15:32
  • "Is Environment a type? A value? Both? This is not clear and error prone."—It's exactly the same as what you get with the enum keyword (there is both a type and a value with the same name). The class keyword also behaves this way. Commented Jul 28 at 22:39
  • You might be interested in the as enum proposal. Commented Jul 28 at 22:40

5 Answers 5

3

I've got a slightly different way to go about this compared to the already existing answers, so I'll add my 2 cents:

const MyEnum = {
    FOO: 0,
    BAR: 1,
    BAZ: 2
} as const;

type MyEnum = (typeof MyEnum)[keyof typeof MyEnum];

export { MyEnum };

To use it elsewhere:

import { MyEnum } from "MyEnum.ts";

const someData: Record<MyEnum, any> = {
    [MyEnum.FOO]: "foo",
    // ...
};

Here I've used the type created as well as the values from it via a single export.

Sign up to request clarification or add additional context in comments.

1 Comment

I really like this answer, nice job!
2

If you require the enum type safety you could mimic that behavior by using branded types. You can define a helper function thats builds you an enum like runtime object which has values of branded strings. This way the values distinguish from string literals.

type Branded<T, Brand extends string> = T & { __brand: Brand };
const brand = "environment";

const makeEnvironment = <const T extends Record<string, string>>(obj: T) => {
  return Object.fromEntries(
    Object.entries(obj).map(([k, v]) => [
      k,
      v as Branded<typeof v, typeof brand>,
    ]),
  ) as {
    [K in keyof T]: Branded<T[K], typeof brand>;
  };
};

const environment = makeEnvironment({
  Development: "development",
  Production: "production",
  Test: "test",
});

type Environment = (typeof environment)[keyof typeof environment];

declare const test: (e: Environment) => void;

test("development");
//   ~~~~~~~~~~~~~ -> Argument of type '"development"'
// is not assignable to parameter of type 'Environment'.
test(environment.Development); // works fine

TypeScript Playground

Comments

1

following @Behemoth's answer, I made it a bit more generic:

--

// makeBrandedType.ts

type Branded<T, Brand extends string> = T & { __brand: Brand };


export const makeBrandedType = <const T extends Record<string, string>>(obj: T, brand : string) => {
    return Object.fromEntries(
        Object.entries(obj).map(([k, v]) => [
            k,
            v as Branded<typeof v, typeof brand>,
        ]),
    ) as {
        [K in keyof T]: Branded<T[K], typeof brand>;
    };
};

and then combined it with the type and value declarations:

// Environment.ts

import {makeBrandedType} from './makeBrandedType.ts';

export const Environment = makeBrandedType({
    Development: "development",
    Production: "production",
    Test: "test",
}, 'environment')  ;

export type Environment = (typeof Environment)[keyof typeof Environment];

This way, although not as simple as classic Enums I get a good types safety and relative ease of use.

Comments

0

The 1.5kB library Superenum makes handling enum like types (including enum if wanted) easy:

import { Enum, EnumType } from '@ncoderz/superenum'

const MyEnum = {
    Red: 0, 
    Green: 1,
    Blue: 2,
}

type MyEnumType = EnumType<typeof MyEnum>;

// Validate input
const validated = Enum(MyEnum).fromValue(1); // MyEnum.Green === 1 
const invalid = Enum(MyEnum).fromValue(99); // undefined
const withDefault = Enum(MyEnum).fromValue(99) ?? MyEnum.Blue; // MyEnum.Blue === 2 


// Iterate
for (const key of Enum(MyEnum).keys()) {
    console.log(key); // 'Red', 'Green', 'Blue'
}
for (const value of Enum(MyEnum).values()) {
    console.log(value); // 0, 1, 2
}
for (const value of Enum(MyEnum).entries()) {
    console.log(value); // ['Red', 0], ['Green', 1], ['Blue', 2]
}

// keys
const valueFromKey = Enum(MyEnum).fromKey('Green'); // MyEnum.Green === 1

// function
const printEnum = (en: MyEnum) => {
    console.log(en);
}

printEnum(MyEnum.Blue) // 2

You can declare enums using enum, objects or arrays and use them all the same way, no matter how declared, all in a type-safe way. The following are all equivalent in the usage and type-safety of MyEnum and its type MyEnumType:

import { Enum, EnumType } from '@ncoderz/superenum'

// enum
enum MyEnum {
    Red: 'Red',  
    Green: 'Green',
    Blue: 'Blue',
}
type MyEnumType = EnumType<typeof MyEnum>;

// object
const MyEnum = {
    Red: 'Red', 
    Green: 'Green',
    Blue: 'Blue',
}
type MyEnumType = EnumType<typeof MyEnum>;

// array
const MyEnum = Enum.fromArray([
    'Red',
    'Green',
    'Blue'
]
type MyEnumType = EnumType<typeof MyEnum>;

P.S. I am the author of the library. I use it extensively with Node 24 for TypeScript only projects without using enum and with --erasableSyntaxOnly

Comments

-1

If you use this code, that error still occurred?


export type EnvironmentTypes {
   Development: string,
   Production: string,
   Test: string,
}

export const Environment: EnvironmentTypes {
   Development: 'development',
   Production: 'production',
   Test: 'test',
}

1 Comment

This, or a similar solution is optional. But... Instead of the one definition that contains the type and the values, I need to write two definitions, and when I use them in the files i need to import both of them... so this could work but feels like a big downgrade...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.