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. As a
popular and heavily used feature of TypeScript, this proposal seeks the adoption of a compatible form of TypeScript's
enum
declaration. Where the syntax or semantics of this proposal differ from that of TypeScript, it is with the full
knowledge of the TypeScript development team and represents either a change that TypeScript is willing to adopt, or
represents a limited subset of functionality that TypeScript expands upon.
NOTE: This proposal has been heavily reworked from its prior incarnation, which can now be found at https://github.com/rbuckton/proposal-enum/tree/old
Stage: 0
Champion: Ron Buckton (@rbuckton)
For more information see the TC39 proposal process.
- Ron Buckton (@rbuckton)
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.)
In addition, with the recent adoption of TypeScript Type Stripping in NodeJS, there has been renewed interest in
ECMA-262 adopting a compatible form of TypeScript's enum
declaration as it is one of the most frequently used
TypeScript features not supported in this mode.
An enum
declaration provides several advantages over an object literal:
- Closed domain by default — The declaration is non-extensible and enum members would be non-configurable and non-writable.
- Restricted domain of allowed values — Restricts initializers to a subset of allowed JS values (
Number
,String
,Symbol
,BigInt
). - Self-references during definition — Referring to prior enum members of the same enum in the initializer of a subsequent enum member.
- Static Typing (tooling) — Static type systems like TypeScript use enum declarations to discriminate types, provide documentation in hovers, etc.
- ADT enums (future) — Potential future support for Algebraic Data Type enums (i.e., "discriminated unions").
- Decorators (future) — Potential future support for
enum
-specific Decorators. - Auto-Initializers (future) — Potential future support for auto-initialized enum members.
- "shared" enums (future) — Potential future support for a
shared enum
with restrictions on inputs to align withshared struct
- TypeScript: Enums
- C++: Enumerations
- C#: Enumeration types
- Java: Enum types
While a Stage 1 proposal is generally encouraged to avoid specific syntax, it is a stated goal of this proposal to
introduce an enum
declaration whose syntax is compatible with TypeScript's. As such, the syntax of this proposal is
restricted to a subset of TypeScript's enum
.
// enum declarations
enum Numbers {
zero = 0,
one = 1,
two = 2,
three = 3,
alsoThree = three // self reference
}
enum PlayState {
idle = "idle",
running = "running",
paused = "paused"
}
enum Symbols {
alpha = Symbol("alpha"),
beta = Symbol("beta")
}
enum Named {
Identifier = 0,
"string name" = 1,
}
// Accessing enum values:
let x = Numbers.three;
let y = Named["string name"];
// Iteration, replaces TypeScript enum "Reverse mapping" (formatting,
// debugging, diagnostics, etc):
for (const [key, value] of Numbers) ...
While a Stage 1 proposal is generally encouraged to avoid specific semantics, it is a stated goal of this proposal to
introduce an enum
declaration whose semantics are compatible with TypeScript's. As such, the semantics of this
proposal are intended to align with that of TypeScript's enum
where possible.
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 whose [[Prototype]] is null
. Each enum member defines
a property on the enum object.
In addition, an enum object contains a Symbol.iterator
method that yields a [key, value]
entry for each declared enum
member, in document order. To explain the semantics of the Symbol.iterator
method, an enum object may require an
[[EnumMembers]] internal slot.
Enum members define the set of values that belong to the enum's domain. Each enum member consists of a name and an
initializer that defines the value associated with that name. Enum members are [[Writable]]: false
and
[[Configurable]]: false
.
Enum member names are currently restricted to IdentifierName and StringLiteral, as those are the only member names
currently supported by TypeScript's enum
. We may opt to expand this to allow other member names like
ComputedPropertyName in the future, if there is sufficient motivation.
An enum may not have duplicate member names. We may opt to introduce restrictions on member names like constructor
if
we deem it necessary to support ADT enums in the future.
If an enum member name shares the same string value as the name of the enum itself, it shadows the enum declaration name within the enum body.
An enum member's initializer is restricted to a subset of
ECMAScript values (i.e., Number
, String
, Symbol
, and BigInt
). This limitation allows us to consider future
support for Algebraic Data Types (ADT) in enums without the possibility of conflicting with something like an enum
member whose value is a function.
An IdentifierReference in an enum member initializer may refer to the name of a prior declaration, and to the enum
itself (much like a class
).
Aside from the enum
declaration itself, there is no other proposed API.
An enum
declaration will have a Symbol.iterator
method that can be used to iterate over the key/value pairs of the enum's
members.
An enum
declaration could potentially be implemented as the following desugaring:
enum E {
A = 1,
B = 2,
C = A | E.B,
}
let E = (() => {
let E = Object.create(null), A, B, C;
Object.defineProperty(E, "A", { value: A = 1 });
Object.defineProperty(E, "B", { value: B = 2 });
Object.defineProperty(E, "C", { value: C = A | E.B });
Object.defineProperty(E, Symbol.iterator, {
value: function* () {
yield ["A", E.A];
yield ["B", E.B];
yield ["C", E.C];
}
});
Object.defineProperty(E, Symbol.toStringTag, { value: "E" });
Object.preventExtensions(E);
return E;
})();
While ECMAScript has both statement and expression forms for class
and function
declarations, this proposal does not
currently support enum
expressions. There is no concept of an enum
expression in TypeScript, though we may consider
enum
expressions for ECMAScript if there is sufficient motivation.
An enum
declaration would support both export
and export default
, much like class
.
In general, this proposal hopes to align enum member values to those that can be shared with a shared struct
, however
it is important to note that a Symbol
-valued enum that uses Symbol()
and not Symbol.for()
would produce values
that cannot be reconciled between two Agents. In addition, ADT enums may need to contain non-shared data, such as in an
Option
or Result
enum. As such, this proposal may seek to introduce a shared enum
declaration that further
restricts allowed inputs.
There are several differences in the enum
declaration for this proposal compared to TypeScript's enum
:
- Auto-initializers
- Declaration merging
- Reverse mapping
const enum
Symbol
valuesBigInt
valuesexport default
TypeScript's enum
supports auto-initialization of enum members:
enum Numbers {
zero, // 0
one, // 1
two // 2
}
However, this behavior is contentious amongst some TC39 delegates and has been removed from this proposal. The main concern that has been raised is that introducing new auto-initialized enum members in the middle of an existing enum has the potential to be a versioning issue in packages, and that such behavior should be harder to reach for, as opposed to being the default behavior. However, even if this capability is not supported, TypeScript will continue to support auto-initialization due to its frequent use within the developer community, but would emit explicit initializers to JavaScript. It is possible that another form of auto-initialization may be introduced in the future that could be utilized by both TypeScript and ECMAScript. For more information, please refer to the Auto-Initializers topic in the Future Directions section.
TypeScript (as of v5.8) allows enum
declarations to merge with other enum
(and namespace
) declarations with the
same name. This is not a desirable feature for ECMAScript enums and will not be supported. TypeScript is considering
deprecating this feature in
TypeScript 6.0.
TypeScript currently supports reverse-mapping enum values back to enum member names using E[value]
, but only for
Number
values. This limitation is intended to avoid collisions for String
-valued enum members that could potentially
overwrite other members. While this information is invaluable for debugging, diagnostics, formatting, and serialization,
it is far less frequently used compared to enum
on the whole.
To avoid this inconsistency, we instead propose using iteration (by way of the Symbol.iterator
built-in
symbol) to cover the "reverse mapping" case:
enum E {
A = 0,
B = "A",
}
for (const [key, value] of E) {
console.log(`${key}: ${value}`);
}
// prints:
// A: 0
// B: A
const keyForA = E[Symbol.iterator]().find(([, value]) => value === "A")[0]
console.log(keyForA); // prints: B
If adopted, TypeScript would add support for Symbol.iterator
while eventually deprecating existing reverse mapping support.
TypeScript supports the concept of a const enum
declaration, which is similar to a normal enum
declaration except
that enum values are inlined into their use sites. Implementations are free to optimize as they see fit, and it's
entirely reasonable that an implementation may eventually support similar inlining on a normal enum
declaration. As
the current const enum
requires whole program knowlege and a type system, we believe it should remain a
TypeScript-specific capability at this time.
TypeScript does not currently support Symbol
values for enums, but would add support if this feature were to be
adopted.
TypeScript does not currently support BigInt
values for enums, but would add support if this feature were to be
adopted.
TypeScript does not currently support export default
on an enum, but would add support if this feature were to be
adopted.
While this proposal is intended to be rather limited in scope, there are several potential areas for future advancement in the form of follow-on proposals:
Algebraic Data Type (ADT) enums act like a discriminated union of structured types. ADT enum members describe a
constructor function that produces an object with a discriminant property. A future enhancement of an
ECMAScript enum
declaration might support ADT enums in conjunction with Extractors and Pattern Matching:
enum Option {
Some(value),
None()
}
const opt = Option.Some(123);
match (opt) {
Option.Some(let value): console.log(value);
Option.None(): console.log("<no value>");
}
enum Result {
Ok(value),
Error(reason)
}
function try_(cb) {
try {
return Result.Ok(_cb());
} catch (e) {
return Result.Error(e);
}
}
const res = try_(() => obj.doWork());
match (res) {
Result.Ok(let value): ...;
Result.Error(let reason): ...;
}
Here, Option.Some
might describe a "constructor" function that produces an object discriminated by either a
well-known symbol field or merely by its [[Prototype]], such that Option.Some(0) instanceof Option.Some
is
true
. ADT enum members could also describe more complex shapes through the use of binding patterns, such as:
enum Geometry {
Point({ x, y }),
Line(p1, p2),
}
const p1 = Geometry.Point({ x: 0, y: 1 });
p1[0].x; // 0
p1[0].y; // 1
const p2 = Geometry.Point({ x: 2, y: 3 });
const l = Geometry.Line(p1, p2);
l[0] === p1; // true
const printGeom = geom => match (geom) {
Geometry.Point({ let x, let y }): console.log(`Point({ x: ${x}, y: ${y} })`);
Geometry.Line(let p1, let p2): console.log(`Line(${printGeom(p1)}, ${printGeom(p2)})`);
};
printGeom(p1); // Point({ x: 0, y: 1 })
printGeom(l); // Line(Point({ x: 0, y: 1 }), Point({ x: 2, y: 3 }))
ADT enum members may also need a mechanism to implement prototypal or static methods on the enum
, which is one
reason why we prefer Symbol.iterator
to describe the domain of an enum
vs. something like Object.entries()
.
In the future we may opt to extend support for Decorators to enum
declarations to support serialization/deserialization, formatting, and FFI scenarios:
@WasmType("u1")
enum Role {
@Alias(["user", "person"], { ignoreCase: true })
user = 1,
@Alias(["admin", "administrator"], { ignoreCase: true })
admin = 2,
}
While this proposal does not support TypeScript's auto-initialization semantics, we may consider introducing an
alternative syntax in a future proposal, such as the of
clause described in an
older version of this proposal:
enum Numbers of Number { zero, one, two, three }
Numbers.zero; // 0
enum Colors of String { red, green, blue }
Colors.red; // "red"
Or through some form of statically recognizable syntax:
auto enum Numbers { zero, one, two, three }
Numbers.zero; // 0
The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
- 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.
- Initial specification text.
- Transpiler support (Optional).
- Complete specification text.
- Designated reviewers have signed off on the current spec text.
- The ECMAScript editor has signed off on the current spec text.
- 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.