Enum unification and improvements by ahejlsberg · Pull Request #50528 · microsoft/TypeScript (original) (raw)
Since #9407 TypeScript has had two kinds of enum types: Numeric enum types and literal enum types (also known as union enum types). Numeric enum types have permitted enum members to have computed values, but not literal types; whereas literal enum types have required members to be declared using simple numeric or string literals only. The distinction between these two kinds of enums is subtle and has been the source of a fair amount of confusion over time.
With this PR we unify the two kinds into a single hybrid that incorporates the best of both worlds. Specifically, enums are always represented as unions of their member types, member values may be computed by constant or non-constant expressions, and members with constant values are given literal types. The PR only affects type checking of enums. There are no changes to the emitted code (except for better inlining of constant expressions in some cases).
An enum declaration now declares a type representing the entire enum and types representing each member (similar to literal enum types). For example,
enum E { A = 10 * 10, // Numeric literal enum member B = "foo", // String literal enum member C = bar(42) // Opaque computed enum member }
declares a type E
and types E.A
, E.B
, and E.C
, where E
is the union E.A | E.B | E.C
.
An enum member has a literal enum type when it is initialized by a constant expression or when it has no initializer and is given an auto-incremented numeric value. In the example above, E.A
and E.B
denote enum literal types with values 100
and "foo"
respectively.
An enum member has a unique and opaque enum member type when it is initialized by a non-constant expression. Non-constant initializer expressions must be assignable to type number
and are not permitted in const
enum declarations. In the example above, E.C
denotes a unique enum member type representing the value computed by the non-constant initializer expression. This unique type is assignable to type number
, but otherwise incompatible with other types.
An expression is considered a constant expression if it is
- a number or string literal,
- a unary
+
,-
, or~
applied to a numeric constant expression, - a binary
+
,-
,*
,/
,%
,**
,<<
,>>
,>>>
,|
,&
,^
applied to two numeric constant expressions, - a binary
+
applied to two constant expressions whereof at least one is a string, - a template expression where each substitution expression is a constant expression,
- a parenthesized constant expression,
- a dotted name (e.g.
x.y.z
) that references aconst
variable with a constant expression initializer and no type annotation, - a dotted name that references an enum member with an enum literal type, or
- a dotted name indexed by a string literal (e.g.
x.y["z"]
) that references an enum member with an enum literal type.
Note that constant expressions are not permitted to make forward references--any value referenced by a constant expression must have been previously declared.
Some examples:
const BaseValue = 10;
const Prefix = "/data";
const enum Values {
First = BaseValue, // 10
Second, // 11
Third // 12
}
const enum Routes {
Parts = ${prefix}/parts
, // "/data/parts"
Invoices = ${prefix}/invoices
// "/data/invoices"
}
NOTE: This PR is technically a breaking change because checking of the new unified enum types is stricter than the old numeric enum types.