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
- C++: Enumerations
- C#: Enumeration types
- Java: Enum types
- TypeScript: Enums
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:
- Derive
key
from the enum member's name. - If the enum member has an Initializer, then
- Set
value
to be the result of evaluating Initializer.
- Set
- Else,
- Set
autoValue
to be ? Call(enumMap
,mapper
, Β«key
,value
,autoValue
Β») - Set
value
to beautoValue
- Set
- Add
key
to the List of member names in the [[EnumMembers]] internal slot of the enum object. - Define a new property on the enum object with the name
key
and the valuevalue
, 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
.
- This member is [[Writable]]:
- 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
.
- This member is [[Writable]]:
- A
@@toStringTag
property whose value is"Enum"
.- This member is [[Writable]]:
false
, [[Configurable]]:true
, and [[Enumerable]]:false
.
- This member is [[Writable]]:
- 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
.
- This member is [[Writable]]:
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:
- If Type(
value
) is not Number, setvalue
toautoValue
. - If
value
isundefined
, return0
. - 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:
- Let
propKey
be ToPropertyKey(key
). - If Type(
propKey
) is Symbol, returnpropKey
.[[Description]]. - 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:
- Let
propKey
be ToPropertyKey(key
). - If Type(
propKey
) is Symbol, letdescription
bepropKey
.[[Description]]. - Otherwise, let
description
bepropKey
. - 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:
- If Type(
value
) is not BigInt, setvalue
toautoValue
. - If
value
isundefined
, return0n
. - 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 anIterator
for the member names in the [[EnumMembers]] internal slot ofE
.Enum.values(E)
- Returns anIterator
for the value onE
of each member in the [[EnumMembers]] internal slot ofE
.Enum.entries(E)
- Returns anIterator
for each member in the [[EnumMembers]] internal slot ofE
, 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)
- Returnstrue
if the the [[EnumMembers]] internal slot ofE
containskey
.Enum.hasValue(E, value)
- Returnstrue
if the [[EnumMembers]] internal slot ofE
contains a member whose value onE
corresponds tovalue
.Enum.getName(E, value)
- Gets the first name in the [[EnumMembers]] internal slot ofE
whose value onE
corresponds tovalue
.Enum.format(E, value)
- Calls the@@formatEnum
method ofE
with argumentvalue
.Enum.parse(E, value)
- Calls the@@parseEnum
method ofE
with argumentvalue
.Enum.create(members)
- Creates an enum object using the property keys and values ofmembers
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
- Initial specification text.
- Transpiler support (Optional).
Stage 3 Entrance Criteria
- Complete specification text.
- Designated reviewers have signed off on the current spec text.
- The ECMAScript editor has signed off on the current spec text.
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.