Home

Awesome

Proposal for ECMAScript enums

A common and oft-used feature of many languages is the concept of an Enumerated Type, or enum. Enums provide a finite domain of constant values that are regularly used to indicate choices, discriminants, and bitwise flags.

Status

Stage: 0
Champion: Ron Buckton (@rbuckton)

For more information see the TC39 proposal process.

Authors

Motivations

Many ECMAScript hosts and libraries have various ways of distinguishing types or operations via some kind of discriminant:

Prior Art

Syntax

// enum declarations

// Each auto-initialized member value is a `Number`, auto-increments values by 1 starting at 0
enum Numbers {
  zero,
  one,
  two,
  three,
  alsoThree = three
}

// Each auto-initialized member value is a `Number`, auto-increments values by 1 starting at 0
enum Colors of Number {
  red,
  green,
  blue
}

// Each auto-initialized member value is a `String` whose value is the SV of its member name.
enum PlayState of String {
  idle,
  running,
  paused
}

// Each auto-initialized member value is a `Symbol` whose description is the SV of its member name.
enum Symbols of Symbol {
  alpha,
  beta
}

enum Named {
  identifierName,
  "string name",
  [expr]
}

// Accessing enum values:
let x = Color.red;
let y = Named["string name"];

Semantics

Well-Known Symbols

This proposal introduces three new well-known symbols that are used with enums:

Specification Name[[Description]]Value and Purpose
@@toEnum"Symbol.toEnum"A method that is used to derive the value for an enum member during EnumMember evaluation.
@@formatEnum"Symbol.formatEnum"A method of an enum object that is used to convert a value into a string representation based on the member names of the enum. Called by Enum.format.
@@parseEnum"Symbol.parseEnum"A method of an enum object that is used to convert a member name String into the value represented by that member of the enum. Called by Enum.parse.

Enum Declarations

Enum declarations consist of a finite set of enum members that define the names and values for each member of the enum. These results are stored as properties of an enum object. An enum object is an ordinary object with an [[EnumMembers]] internal slot, and whose [[Prototype]] is null.

Automatic Initialization

If an enum member does not supply an Initializer, the value of that enum member will be automatically initialized:

enum DaysOfTheWeek {
  Sunday, // 0
  Monday, // 1
  Tuesday, // 2
  // etc.
}

Auto-initialization can be controlled through the use of an of clause:

enum DaysOfTheWeek of Symbol {
  Sunday, // Symbol("Sunday")
  Monday, // Symbol("Monday")
  Tuesday, // Symbol("Tuesday")
  // etc.
}

Constructors for built-in primitive values like String, Number, Symbol, and BigInt are defined to have a @@toEnum method that is used during evaluation to select an auto-initialization value. If the expression in the of clause does not have a @@toEnum method, it will instead be called directly. This allows constructors for built-ins to be used in the of clause without adding a niche constructor overload. This also allows developers to control the behavior of of if its expression is an ECMAScript class which cannot be called directly.

Evaluation

Before we evaluate the enum members of the declaration, we first choose a mapper Object. If the enum declaration has an of clause, the mapper is the result of evaluating that clause. Otherwise, mapper uses the default value of %Number%.

From the mapper we then get an enumMap function from mapper[@@toEnum]. If enumMap is undefined, then we set enumMap to mapper and mapper to undefined.

To support auto-initialization we also define two variables (both initialized to undefined):

As we evaluate each enum member, we perform the following steps:

  1. Derive key from the enum member's name.
  2. If the enum member has an Initializer, then
    1. Set value to be the result of evaluating Initializer.
  3. Else,
    1. Set autoValue to be ? Call(enumMap, mapper, « key, value, autoValue »)
    2. Set value to be autoValue
  4. Add key to the List of member names in the [[EnumMembers]] internal slot of the enum object.
  5. Define a new property on the enum object with the name key and the value value, and the attributes [[Writable]]: false, [[Configurable]]: false, and [[Enumerable]]: true.

In addition, the following additional properties are added to enum objects:

Finally, the enum object is made non-extensible.

Properties of the Number Constructor

The Number constructor would have an additional @@toEnum method with parameters key, value, and autoValue that performs the following steps:

  1. If Type(value) is not Number, set value to autoValue.
  2. If value is undefined, return 0.
  3. Otherwise, return value + 1.

Properties of the String Constructor

The String constructor would have an additional @@toEnum method with parameters key, value, and autoValue that performs the following steps:

  1. Let propKey be ToPropertyKey(key).
  2. If Type(propKey) is Symbol, return propKey.[[Description]].
  3. Otherwise, return propKey.

Properties of the Symbol Constructor

The Symbol constructor would have an additional @@toEnum method that parameters key, value, and autoValue that performs the following steps:

  1. Let propKey be ToPropertyKey(key).
  2. If Type(propKey) is Symbol, let description be propKey.[[Description]].
  3. Otherwise, let description be propKey.
  4. Return a new unique Symbol whose [[Description]] value is description.

Properties of the BigInt Constructor

The BigInt constructor would have an additional @@toEnum method with parameters key, value, and autoValue that performs the following steps:

  1. If Type(value) is not BigInt, set value to autoValue.
  2. If value is undefined, return 0n.
  3. Otherwise, return value + 1n.

API

To make it easier to work with enums, an Enum object is added to the global scope, with the following methods:

let Enum: {
  keys(E: object): IterableIterator<string | symbol>;
  values(E: object): IterableIterator<any>;
  entries(E: object): IterableIterator<[string | symbol, any]>;
  has(E: object, key: string | symbol): boolean;
  hasValue(E: object, value: any): boolean;
  getName(E: object, value: any): string | undefined;
  format(E: object, value: any): string | symbol | undefined;
  parse(E: object, value: string): any;
  create(members: object): object;
  flags(descriptor: EnumDescriptor): EnumDescriptor;
};
<!-- # Grammar ```grammarkdown ``` -->

Examples

<!-- Examples of the proposal -->
enum Numbers { zero, one, two, three, }

typeof Numbers.zero === "number"
Numbers.zero === 0
Enum.getName(Numbers, 0) === "zero"
Enum.parse(Numbers, "zero") === 0

// ... strings, ...
enum HttpMethods of String { GET, PUT, POST, DELETE }

typeof HttpMethods.GET === "string"
HttpMethods.GET === "GET"

// ... booleans, ...
enum Switch { on = true, off = false }

typeof Switch.on === "boolean";
Switch.on === true

// ... symbols, ...
enum AlphaBeta of Symbol { alpha, beta }

typeof AlphaBeta.alpha === "symbol";
AlphaBeta.alpha.toString() === "Symbol(AlphaBeta.alpha)";

// ... or a mix.
enum Mixed {
    number = 0,
    string = "",
    boolean = false,
    symbol = Symbol()
}

// Enums can be exported:
export enum Zoo { lion, tiger, bear };
export default enum { up, down, left, right };

// You can test for name membership using `Enum.has()`
Enum.has(Numbers, "one") === true
Enum.has(Numbers, "five") === false

// You can test for value membership using `Enum.hasValue()`:
Enum.hasValue(Numbers, 0) === true
Enum.hasValue(Numbers, 9) === false

// You can convert enums between names and values using 
// `Enum.parse` and `Enum.format`, respectively.
enum AToB {
    a = "b",
    b = "a",
}

Enum.parse(AToB, "a") === AToB.a
Enum.parse(AToB, "b") === AToB.b

Enum.getName(AToB, AToB.a) === "b"
Enum.getName(AToB, AToB, b) === "a"

// `Enum.create()` lets you create a new enum programmatically:
const SyntaxKind = Enum.create({ 
  identifier: 0, 
  number: 1, 
  string: 2 
});

typeof SyntaxKind.identifier === "number";
SyntaxKind.identifier === 0;


// The `Enum.flags` decorator lets you declare a enum containing 
// bitwise flag values:
@Enum.flags
enum FileMode {
  none
  read,
  write,
  exclusive,
  readWrite = read | write,
}

FileMode.none === 0x0
FileMode.readOnly === 0x1
FileMode.readWrite === 0x3

// `Enum.flags` modifies @@formatEnum:
Enum.format(FileMode, FileMode.readWrite | FileMode.exclusive) === "readWrite, exclusive"

// `EnumFlags` modifies @@parseEnum:
Enum.parse(FileMode, "read, 4") === 5 // FileMode.read | FileMode.exclusive

Remarks

TODO

The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:

Stage 1 Entrance Criteria

Stage 2 Entrance Criteria

Stage 3 Entrance Criteria

Stage 4 Entrance Criteria

<!-- # References --> <!-- Links to other specifications, etc. --> <!-- * [Title](url) -->

Prior Discussion

<!-- Links to prior discussion topics on https://esdiscuss.org --> <!-- The following are shared links used throughout the README: -->