Repository: TypeScript
Issue: Proposal: strictly-typed enums
Labels: Suggestion,Waiting for TC39

# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## 🔍 Search Terms

<!--
  💡 Did you know? TypeScript has over 2,000 open suggestions!
  🔎 Please search thoroughly before logging new feature requests as most common ideas already have a proposal in progress.
  The "Common Feature Requests" section of the FAQ lists many popular requests: https://github.com/Microsoft/TypeScript/wiki/FAQ#common-feature-requests

  Replace the text below:
-->

enum single type syntax

## ✅ Viability Checklist

<!--
   Suggestions that don't meet all these criteria are very, very unlikely to be accepted.
   We always recommend reviewing the TypeScript design goals before investing time writing
   a proposal for ideas outside the scope of the project.
-->
My suggestion meets these guidelines:

* [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
* [x] This wouldn't change the runtime behavior of existing JavaScript code
* [x] This could be implemented without emitting different JS based on the types of the expressions
* [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
* [x] This feature would agree with the rest of [TypeScript's Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals).


## ⭐ Suggestion

<!-- A summary of what you'd like to see added or changed -->
I'd like to propose the addition of additional syntax for enums to help constrain them and also allow some DevX improvements.

I've drawn inspiration from flow's syntax for enums as I feel like it's a minimal addition to the syntax whilst also being clear.

```ts
enum StringEnum of string {
  A,
  B,
  C,
}

enum NumberEnum of number {
  A,
  B,
  C,
}
```

### Syntax

```
<enum-declaration> ::= "enum" <Identifier> "{" | "enum" <Identifier> "of" <enum-type> "{"

<Identifier> ::= any name that is a valid JS Identifier

<enum-type> ::= "number" | "string"
```

### Allowed Types

As per the types currently allowed by TS, the allowed `enum-type` values may only be `string` or `number`. In future one could consider extending this to include `symbol` or `boolean` to match Flow, but for now we'd just be looking for parity.

When declared with `of string`, all enum members **must** be strings. Similarly when declared with `of number`, all enum members **must** be numbers.

### Value Uniqueness

When declared with `of`, all enum members **must** be declared with a unique value.
This constraint also means that referencing other members in the enum is no longer valid, and thus TS need not declare a scope or variables for the enum names.

```ts
// regular syntax
enum Foo {
  A = 'A',
  B = A,     // === 'A'
  C = Foo.A, // === 'A'
}
```

### Defaulted vs Initialised Members

#### String

When declared with `of string`, any defaulted enum members will have a value equal to the string value of the enum name. Any initialised members will have the value as specified.

```ts
enum Foo of string {
  A,         // === 'A'
  BBB_BBB,   // === 'BBB_BBB'
  C = 'Bar', // === 'Bar'
}
```

#### Number

When specified with `of number`, defaulted members will be set to an integer, starting at 0 and incremented for each member.
An enum may declare default or initialised members, though defaulted members may not be used after an initialised member. This constraint exists to make it easier to reason about value uniqueness and ordering.

```ts
// ✅ Valid
enum Foo of number {
  A, // === 0
  B, // === 1
  C, // === 2
}

// ✅ Valid
enum Foo of number {
  A = 1,
  B = 2,
  C = 3,
}

// ✅ Valid
enum Foo of number {
  A, // === 0
  B, // === 1
  C = 3,
}

// ❌ Invalid
enum Foo of number {
  A = 1,
  B,
  C,
}
```

#### Nominally Typed

Comparisons and assignments of non-enum values to locations typed with an `of` enum are not allowed.  

```ts
enum Foo of number { A, B, C }
let foo: Foo = Foo.A;
foo = 1;             // ❌ Invalid
foo += 1;            // ❌ Invalid
foo === 1;           // ❌ Invalid
foo = Foo.A | Foo.B; // ❌ Invalid
declare function acceptsFoo(arg: Foo): void;
acceptsFoo(1);       // ❌ Invalid

enum Bar of string { A, B, C }
let bar: Bar = Bar.A;
bar = 'B';       // ❌ Invalid
bar += 'B';      // ❌ Invalid
bar === 'B';     // ❌ Invalid
declare function acceptsBar(arg: Bar): void;
acceptsBar('B'); // ❌ Invalid
```

However, enums may be assigned to locations expecting their base types, or may be used in ways afforded by their base types:
```ts
enum Foo of number { A, B, C }
const math = Foo.A + Foo.B;  // ✅ Valid
const str = Foo.A.toFixed(); // ✅ Valid

enum Bar of string { A }
const a = Foo.A.charAt(0);   // ✅ Valid

declare function acceptsString(arg: string): void;
acceptsString(Bar.A);        // ✅ Valid
```

## 💻 Use Cases

<!--
  What do you want to use this for?
  What shortcomings exist with current approaches?
  What workarounds are you using in the meantime?
-->

The big things I'm looking to achieve with this proposal are as follows:
1) provide a way to auto-default string members
    - currently if you want a string enum, you need to specify an initialiser for all enum members, otherwise TS defaults to strings.
    - this is a pretty cumbersome devx for what is a really common usecase.
1) provide a way to allow users to opt-in to singly-typed enums
    - from my experience it's a very rare case in which you want to use a mixed-type enum, so being able to opt-in to stricter declarations would be good for consistency and correctness.
1) provide a way to allow users to opt-in to stricter enums
    - as described in many issues and gripes, TS enums are very loose right now and allow a lot of things that are considered loose (eg `foo = 'A'` or `foo === 'A'` being valid if there's a member with the value `'A'`) or even dangerous (eg `foo = 99` if there are ***any*** number-typed members, even if there are no members with the value `99`).
    - it's too big of a change to breaking change the base enum logic, so my thinking is that allowing users to opt-in we can help the ecosystem move to a stricter and safer future without breaking old code.