const
enums
keyof
and enumsThis chapter answers the following two questions:
In the next chapter, we take a look at alternatives to enums.
boolean
is a type with a finite amount of values: false
and true
. With enums, TypeScript lets us define similar types ourselves.
This is a numeric enum:
enum NoYes {= 0,
No = 1, // trailing comma
Yes
}
.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1); assert
Explanations:
No
and Yes
are called the members of the enum NoYes
.No
and the value 0
.We can use members as if they were literals such as true
, 123
, or 'abc'
– for example:
function toGerman(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return
}
}.equal(toGerman(NoYes.No), 'Nein');
assert.equal(toGerman(NoYes.Yes), 'Ja'); assert
Instead of numbers, we can also use strings as enum member values:
enum NoYes {= 'No',
No = 'Yes',
Yes
}
.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes'); assert
The last kind of enums is called heterogeneous. The member values of a heterogeneous enum are a mix of numbers and strings:
enum Enum {= 'One',
One = 'Two',
Two = 3,
Three = 4,
Four
}.deepEqual(
assert.One, Enum.Two, Enum.Three, Enum.Four],
[Enum'One', 'Two', 3, 4]
[; )
Heterogeneous enums are not used often because they have few applications.
Alas, TypeScript only supports numbers and strings as enum member values. Other values, such as symbols, are not allowed.
We can omit initializers in two cases:
This is a numeric enum without any initializers:
enum NoYes {,
No,
Yes
}.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1); assert
This is a heterogeneous enum where some initializers are omitted:
enum Enum {,
A,
B= 'C',
C = 'D',
D = 8, // (A)
E ,
F
}.deepEqual(
assert.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
[Enum0, 1, 'C', 'D', 8, 9]
[; )
Note that we can’t omit the initializer in line A because the value of the preceding member is not a number.
There are several precedents for naming constants (in enums or elsewhere):
Number.MAX_VALUE
Math.SQRT2
Symbol.asyncIterator
NoYes
enum.Similar to JavaScript objects, we can quote the names of enum members:
enum HttpRequestField {'Accept',
'Accept-Charset',
'Accept-Datetime',
'Accept-Encoding',
'Accept-Language',
}.equal(HttpRequestField['Accept-Charset'], 1); assert
There is no way to compute the names of enum members. Object literals support computed property keys via square brackets.
TypeScript distinguishes three kinds of enum members, by how they are initialized:
A literal enum member:
A constant enum member is initialized via an expression whose result can be computed at compile time.
A computed enum member is initialized via an arbitrary expression.
So far, we have only used literal members.
In the previous list, members that are mentioned earlier are less flexible but support more features. Read on for more information.
An enum member is literal if its value is specified:
If an enum has only literal members, we can use those members as types (similar to how, e.g., number literals can be used as types):
enum NoYes {= 'No',
No = 'Yes',
Yes
}function func(x: NoYes.No) { // (A)
;
return x
}
func(NoYes.No); // OK
// @ts-expect-error: Argument of type '"No"' is not assignable to
// parameter of type 'NoYes.No'.
func('No');
// @ts-expect-error: Argument of type 'NoYes.Yes' is not assignable to
// parameter of type 'NoYes.No'.
func(NoYes.Yes);
NoYes.No
in line A is an enum member type.
Additionally, literal enums support exhaustiveness checks (which we’ll look at later).
An enum member is constant if its value can be computed at compile time. Therefore, we can either specify its value implicitly (that is, we let TypeScript specify it for us). Or we can specify it explicitly and are only allowed to use the following syntax:
+
, -
, ~
+
, -
, *
, /
, %
, <<
, >>
, >>>
, &
, |
, ^
This is an example of an enum whose members are all constant (we’ll see later how that enum is used):
enum Perm {= 1 << 8, // bit 8
UserRead = 1 << 7,
UserWrite = 1 << 6,
UserExecute = 1 << 5,
GroupRead = 1 << 4,
GroupWrite = 1 << 3,
GroupExecute = 1 << 2,
AllRead = 1 << 1,
AllWrite = 1 << 0,
AllExecute }
In general, constant members can’t be used as types. However, exhaustiveness checks are still performed.
The values of computed enum members can be specified via arbitrary expressions. For example:
enum NoYesNum {= 123,
No = Math.random(), // OK
Yes }
This was a numeric enum. String-based enums and heterogeneous enums are more limited. For example, we cannot use method invocations to specify member values:
enum NoYesStr {= 'No',
No // @ts-expect-error: Computed values are not permitted in
// an enum with string valued members.
= ['Y', 'e', 's'].join(''),
Yes }
TypeScript does not do exhaustiveness checks for computed enum members.
When logging members of numeric enums, we only see numbers:
, Yes }
enum NoYes { No
console.log(NoYes.No);
console.log(NoYes.Yes);
// Output:
// 0
// 1
When using the enum as a type, the values that are allowed statically are not just those of the enum members – any number is accepted:
, Yes }
enum NoYes { Nofunction func(noYes: NoYes) {}
func(33); // no error!
Why aren’t there stricter static checks? Daniel Rosenwasser explains:
The behavior is motivated by bitwise operations. There are times when
SomeFlag.Foo | SomeFlag.Bar
is intended to produce anotherSomeFlag
. Instead you end up withnumber
, and you don’t want to have to cast back toSomeFlag
.I think if we did TypeScript over again and still had enums, we’d have made a separate construct for bit flags.
How enums are used for bit patterns is demonstrated soon in more detail.
My recommendation is to prefer string-based enums (for brevity’s sake, this chapter doesn’t always follow this recommendation):
='No', Yes='Yes' } enum NoYes { No
On one hand, logging output is more useful for humans:
console.log(NoYes.No);
console.log(NoYes.Yes);
// Output:
// 'No'
// 'Yes'
On the other hand, we get stricter type checking:
function func(noYes: NoYes) {}
// @ts-expect-error: Argument of type '"abc"' is not assignable
// to parameter of type 'NoYes'.
func('abc');
// @ts-expect-error: Argument of type '"Yes"' is not assignable
// to parameter of type 'NoYes'.
func('Yes'); // (A)
Not even strings that are equal to values of members are allowed (line A).
In the Node.js file system module, several functions have the parameter mode
. It specifies file permissions, via a numeric encoding that is a holdover from Unix:
That means that permissions can be represented by 9 bits (3 categories with 3 permissions each):
User | Group | All | |
---|---|---|---|
Permissions | r, w, x | r, w, x | r, w, x |
Bit | 8, 7, 6 | 5, 4, 3 | 2, 1, 0 |
Node.js doesn’t do this, but we could use an enum to work with these flags:
enum Perm {= 1 << 8, // bit 8
UserRead = 1 << 7,
UserWrite = 1 << 6,
UserExecute = 1 << 5,
GroupRead = 1 << 4,
GroupWrite = 1 << 3,
GroupExecute = 1 << 2,
AllRead = 1 << 1,
AllWrite = 1 << 0,
AllExecute }
Bit patterns are combined via bitwise Or:
// User can change, read and execute.
// Everyone else can only read and execute.
.equal(
assert.UserRead | Perm.UserWrite | Perm.UserExecute |
Perm.GroupRead | Perm.GroupExecute |
Perm.AllRead | Perm.AllExecute,
Perm0o755);
// User can read and write.
// Group members can read.
// Everyone can’t access at all.
.equal(
assert.UserRead | Perm.UserWrite | Perm.GroupRead,
Perm0o640);
The main idea behind bit patterns is that there is a set of flags and that any subset of those flags can be chosen.
Therefore, using real sets to choose subsets is a more straightforward way of performing the same task:
enum Perm {= 'UserRead',
UserRead = 'UserWrite',
UserWrite = 'UserExecute',
UserExecute = 'GroupRead',
GroupRead = 'GroupWrite',
GroupWrite = 'GroupExecute',
GroupExecute = 'AllRead',
AllRead = 'AllWrite',
AllWrite = 'AllExecute',
AllExecute
}function writeFileSync(
: string, permissions: Set<Perm>, content: string) {
thePath// ···
}writeFileSync(
'/tmp/hello.txt',
new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
'Hello!');
Sometimes, we have sets of constants that belong together:
= Symbol('off');
const off = Symbol('info');
const info = Symbol('warn');
const warn = Symbol('error'); const error
This is a good use case for an enum:
enum LogLevel {= 'off',
off = 'info',
info = 'warn',
warn = 'error',
error }
One benefit of the enum is that the constant names are grouped and nested inside the namespace LogLevel
.
Another one is that we automatically get the type LogLevel
for them. If we want such a type for the constants, we need more work:
type LogLevel =
| typeof off
| typeof info
| typeof warn
| typeof error
;
For more information on this approach, see §13.1.3 “Unions of symbol singleton types”.
When booleans are used to represent alternatives, enums are usually more self-descriptive.
For example, to represent whether a list is ordered or not, we can use a boolean:
class List1 {: boolean;
isOrdered// ···
}
However, an enum is more self-descriptive and has the additional benefit that we can add more alternatives later if we need to.
, unordered }
enum ListKind { ordered
class List2 {: ListKind;
listKind// ···
}
Similarly, we can specify how to handle errors via a boolean value:
function convertToHtml1(markdown: string, throwOnError: boolean) {
// ···
}
Or we can do so via an enum value:
enum ErrorHandling {= 'throwOnError',
throwOnError = 'showErrorsInContent',
showErrorsInContent
}function convertToHtml2(markdown: string, errorHandling: ErrorHandling) {
// ···
}
Consider the following function that creates regular expressions.
= 'g';
const GLOBAL = '';
const NOT_GLOBAL type Globalness = typeof GLOBAL | typeof NOT_GLOBAL;
function createRegExp(source: string,
: Globalness = NOT_GLOBAL) {
globalnessnew RegExp(source, 'u' + globalness);
return
}
.deepEqual(
assertcreateRegExp('abc', GLOBAL),
/abc/ug);
.deepEqual(
assertcreateRegExp('abc', 'g'), // OK
/abc/ug);
Instead of the string constants, we can use an enum:
enum Globalness {= 'g',
Global = '',
notGlobal
}
function createRegExp(source: string, globalness = Globalness.notGlobal) {
new RegExp(source, 'u' + globalness);
return
}
.deepEqual(
assertcreateRegExp('abc', Globalness.Global),
/abc/ug);
.deepEqual(
assert// @ts-expect-error: Argument of type '"g"' is not assignable to parameter of type 'Globalness | undefined'. (2345)
createRegExp('abc', 'g'), // error
/abc/ug);
What are the benefits of this approach?
Globalness
only accepts member names, not strings.TypeScript compiles enums to JavaScript objects. As an example, take the following enum:
enum NoYes {,
No,
Yes }
TypeScript compiles this enum to:
var NoYes;
function (NoYes) {
("No"] = 0] = "No";
NoYes[NoYes["Yes"] = 1] = "Yes";
NoYes[NoYes[|| (NoYes = {})); })(NoYes
In this code, the following assignments are made:
"No"] = 0;
NoYes["Yes"] = 1;
NoYes[
0] = "No";
NoYes[1] = "Yes"; NoYes[
There are two groups of assignments:
Given a numeric enum:
enum NoYes {,
No,
Yes }
The normal mapping is from member names to member values:
// Static (= fixed) lookup:
.equal(NoYes.Yes, 1);
assert
// Dynamic lookup:
.equal(NoYes['Yes'], 1); assert
Numeric enums also support a reverse mapping from member values to member names:
.equal(NoYes[1], 'Yes'); assert
One use case for reverse mappings is printing the name of an enum member:
function getQualifiedName(value: NoYes) {
'NoYes.' + NoYes[value];
return
}.equal(
assertgetQualifiedName(NoYes.Yes), 'NoYes.Yes');
String-based enums have a simpler representation at runtime.
Consider the following enum.
enum NoYes {= 'NO!',
No = 'YES!',
Yes }
It is compiled to this JavaScript code:
var NoYes;
function (NoYes) {
("No"] = "NO!";
NoYes["Yes"] = "YES!";
NoYes[|| (NoYes = {})); })(NoYes
TypeScript does not support reverse mappings for string-based enums.
const
enumsIf an enum is prefixed with the keyword const
, it doesn’t have a representation at runtime. Instead, the values of its member are used directly.
To observe this effect, let us first examine the following non-const enum:
enum NoYes {= 'No',
No = 'Yes',
Yes
}
function toGerman(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return
} }
TypeScript compiles this code to:
"use strict";
var NoYes;
function (NoYes) {
("No"] = "No";
NoYes["Yes"] = "Yes";
NoYes[|| (NoYes = {}));
})(NoYes
function toGerman(value) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
} }
This is the same code as previously, but now the enum is const:
const enum NoYes {,
No,
Yes
}function toGerman(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return
} }
Now the representation of the enum as a construct disappears and only the values of its members remain:
function toGerman(value) {
switch (value) {
case "No" /* No */:
return 'Nein';
case "Yes" /* Yes */:
return 'Ja';
} }
TypeScript treats (non-const) enums as if they were objects:
enum NoYes {= 'No',
No = 'Yes',
Yes
}function func(obj: { No: string }) {
.No;
return obj
}.equal(
assertfunc(NoYes), // allowed statically!
'No');
When we accept an enum member value, we often want to make sure that:
Read on for more information. We will be working with the following enum:
enum NoYes {= 'No',
No = 'Yes',
Yes }
In the following code, we take two measures against illegal values:
function toGerman1(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return default:
new TypeError('Unsupported value: ' + JSON.stringify(value));
throw
}
}
.throws(
assert// @ts-expect-error: Argument of type '"Maybe"' is not assignable to
// parameter of type 'NoYes'.
=> toGerman1('Maybe'),
() /^TypeError: Unsupported value: "Maybe"$/);
The measures are:
NoYes
prevents illegal values being passed to the parameter value
.default
case is used to throw an exception if there is an unexpected value.We can take one more measure. The following code performs an exhaustiveness check: TypeScript will warn us if we forget to consider all enum members.
class UnsupportedValueError extends Error {constructor(value: never) {
super('Unsupported value: ' + value);
}
}
function toGerman2(value: NoYes) {
switch (value) {
.No:
case NoYes'Nein';
return .Yes:
case NoYes'Ja';
return default:
new UnsupportedValueError(value);
throw
} }
How does the exhaustiveness check work? For every case, TypeScript infers the type of value
:
function toGerman2b(value: NoYes) {
switch (value) {
.No:
case NoYes// %inferred-type: NoYes.No
;
value'Nein';
return .Yes:
case NoYes// %inferred-type: NoYes.Yes
;
value'Ja';
return default:
// %inferred-type: never
;
valuenew UnsupportedValueError(value);
throw
} }
In the default case, TypeScript infers the type never
for value
because we never get there. If however, we add a member .Maybe
to NoYes
, then the inferred type of value
is NoYes.Maybe
. And that type is statically incompatible with the type never
of the parameter of new UnsupportedValueError()
. That’s why we get the following error message at compile time:
Argument of type 'NoYes.Maybe' is not assignable to parameter of type 'never'.
Conveniently, this kind of exhaustiveness check also works with if
statements:
function toGerman3(value: NoYes) {
if (value === NoYes.No) {
'Nein';
return if (value === NoYes.Yes) {
} else 'Ja';
return
} else {new UnsupportedValueError(value);
throw
} }
Alternatively, we also get an exhaustiveness check if we specify a return type:
function toGerman4(value: NoYes): string {
switch (value) {
.No:
case NoYes: NoYes.No = value;
const x'Nein';
return .Yes:
case NoYes: NoYes.Yes = value;
const y'Ja';
return
} }
If we add a member to NoYes
, then TypeScript complains that toGerman4()
may return undefined
.
Downsides of this approach:
if
statements (more information).keyof
and enumsWe can use the keyof
type operator to create the type whose elements are the keys of the enum members. When we do so, we need to combine keyof
with typeof
:
enum HttpRequestKeyEnum {'Accept',
'Accept-Charset',
'Accept-Datetime',
'Accept-Encoding',
'Accept-Language',
}// %inferred-type: "Accept" | "Accept-Charset" | "Accept-Datetime" |
// "Accept-Encoding" | "Accept-Language"
type HttpRequestKey = keyof typeof HttpRequestKeyEnum;
function getRequestHeaderValue(request: Request, key: HttpRequestKey) {
// ···
}
keyof
without typeof
If we use keyof
without typeof
, we get a different, less useful, type:
// %inferred-type: "toString" | "toFixed" | "toExponential" |
// "toPrecision" | "valueOf" | "toLocaleString"
type Keys = keyof HttpRequestKeyEnum;
keyof HttpRequestKeyEnum
is the same as keyof number
.
@spira_mirabilis
for their feedback to this chapter.