Skip to content

rbuckton/proposal-enum

Repository files navigation

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

  • Ron Buckton (@rbuckton)

Motivations

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

  • ECMAScript:
    • [Symbol.toStringTag]
    • typeof
  • DOM:
    • Node.prototype.nodeType (Node.ATTRIBUTE_NODE, Node.CDATA_SECTION_NODE, etc.)
    • DOMException.prototype.code (DOMException.ABORT_ERR, DOMException.DATA_CLONE_ERR, etc.)
    • XMLHttpRequest.prototype.readyState (XMLHttpRequest.DONE, XMLHttpRequest.HEADERS_RECEIVED, etc.)
    • CSSRule.prototype.type (CSSRule.CHARSET_RULE, CSSRule.FONT_FACE_RULE, etc.)
    • Animation.prototype.playState ("idle", "running", "paused", "finished")
    • ApplicationCache.prototype.status (ApplicationCache.CHECKING, ApplicationCache.DOWNLOADING, etc.)
  • NodeJS:
    • Buffer encodings ("ascii", "utf8", "base64", etc.)
    • os.platform() ("win32", "linux", "darwin", etc.)
    • "constants" module (ENOENT, EEXIST, etc.; S_IFMT, S_IFREG, etc.)

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):

  • value: Stores the result of the last explicit or automatic initialization.
  • autoValue: Stores the result of the last automatic initialization only.

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:

  • A @@parseEnum property whose value is a Function that returns the value of the enum member whose name corresponds to the provided argument.
    • This member is [[Writable]]: false, [[Configurable]]: true, and [[Enumerable]]: false.
  • A @@formatEnum property whose value is a Function that returns the name of the first enum member whose value corresponds to the provided argument.
    • This member is [[Writable]]: false, [[Configurable]]: true, and [[Enumerable]]: false.
  • A @@toStringTag property whose value is "Enum".
    • This member is [[Writable]]: false, [[Configurable]]: true, and [[Enumerable]]: false.
  • An @@iterator property whose value is a Function that returns an iterator for this enum's [[EnumMembers]] internal slot where each yielded value is a two-element array containing the enum member name at index 0 and the enum member value at index 1.
    • This member is [[Writable]]: false, [[Configurable]]: true, and [[Enumerable]]: false.

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:

  • Enum.keys(E) - Returns an Iterator for the member names in the [[EnumMembers]] internal slot of E.
  • Enum.values(E) - Returns an Iterator for the value on E of each member in the [[EnumMembers]] internal slot of E.
  • Enum.entries(E) - Returns an Iterator for each member in the [[EnumMembers]] internal slot of E, where each result is two-element array containing the enum member name at index 0 and the enum member value at index 1.
  • Enum.has(E, key) - Returns true if the the [[EnumMembers]] internal slot of E contains key.
  • Enum.hasValue(E, value) - Returns true if the [[EnumMembers]] internal slot of E contains a member whose value on E corresponds to value.
  • Enum.getName(E, value) - Gets the first name in the [[EnumMembers]] internal slot of E whose value on E corresponds to value.
  • Enum.format(E, value) - Calls the @@formatEnum method of E with argument value.
  • Enum.parse(E, value) - Calls the @@parseEnum method of E with argument value.
  • Enum.create(members) - Creates an enum object using the property keys and values of members as the enum members for the new enum.
  • Enum.flags(descriptor) - A built-in decorator that modifies the enum object in the following ways:
    • The auto-increment behavior is changed to shift the current auto-increment value left by 1.
    • The @@parseEnum method is modified to parse a comma-separated string and OR the resulting values together. If no corresponding name can be found and the name can be successfully coerced to a number, that number is OR'ed with the result.
    • The @@formatEnum method is modified to convert a bitwise combination of flag values into a comma separated string of corresponding names. If no corresponding name can be found, the SV of the bits is appended to the string.
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;
};

Examples

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

  • Why default to Number?
    • In prior discussions, there are some preferences for the use of symbol values, while there are other preferences that include the use of strings and numbers. This approach gives you the ability to support both scenarios through the optional of clause.

    • The auto-increment behavior of enums in other languages is used fairly regularly. Auto- increment is not viable if String or Symbol were the default type.

    • We could consider switching on auto-increment if the prior declaration was initialized with a Number, but then you would have confusion over declarations like this:

      enum Mixed {
        first, // If this is a Symbol by default...
        second = 1,
        third // ...is this a Symbol or the Number `2`?
      }

TODO

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

Stage 1 Entrance Criteria

  • Identified a "champion" who will advance the addition.
  • Prose outlining the problem or need and the general shape of a solution.
  • Illustrative examples of usage.
  • High-level API.

Stage 2 Entrance Criteria

Stage 3 Entrance Criteria

Stage 4 Entrance Criteria

  • Test262 acceptance tests have been written for mainline usage scenarios and merged.
  • Two compatible implementations which pass the acceptance tests: [1], [2].
  • A pull request has been sent to tc39/ecma262 with the integrated spec text.
  • The ECMAScript editor has signed off on the pull request.

Prior Discussion