- 4 years ago
- Afaq Arif
- 2,744 Views
-
2
In this chapter, we will discuss enums in TypeScript with examples. We have also discussed the types of enums in TypeScript in detail.
Enums are one of the few characteristics TypeScript has which is not a type-level extension of JavaScript.
Enums allow a developer to state a set of named constants. It is easier to document a plan, or create a set of different cases while using enums. . TypeScript gives both numeric and string-based enums.
Numeric enums
We’ll first start off with numeric enums, which are probably more familiar if you’re coming from other languages. An enum can be stated using the enum
keyword.
enum Direction {
Up = 1,
Down,
Left,
Right,
}
We have stated a numeric enum Up with 1 above. All of the following members are auto-increased from that point on. In other words, Direction.Up
has the value 1
, Down
has 2
, Left
has 3
, and Right
has 4
.
If we want, we can leave off the initializers entirely.
enum Direction {
Up,
Down,
Left,
Right,
}
Here, Up
would have the value 0
, Down
would have 1
, etc. This auto-increasing behavior is might useful for us where we actually not care about the member values themselves, but do value that each value is different from other values in the same enum.
Using an enum is simple: just access any member as a property off of the enum itself, and declare types using the name of the enum.
enum UserResponse {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: UserResponse): void {
// ...
}
respond("Princess Caroline", UserResponse.Yes);
We can mix numeric enums in computed and constant members (see below). The short story is enums without initializers either need to be original or have to come after numeric enums initialized with numeric constants or other constant enum members. In other words, we don’t allow the following.
enum E {
A = getSomeValue(),
B,
Enum member must have initializer.
}
String enums
String enums are a related idea but have some definite runtime differences as documented below. In a string enum, each member has to be constant-initialized with a string literal, or with another string enum member.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
String enums have the advantage that they “serialize” well as they don’t have auto-increasing behavior. In other words, if you were debugging and had to read the runtime value of a numeric enum, the value is usually opaque – it doesn’t send any useful meaning on its own (though reverse mapping can often help), string enums allow you to give a meaningful and readable value when your code runs, independent of the name of the enum member itself.
Heterogeneous enums
We can mix enums with string and numeric members technically, but it’s not clear why you would ever want to do so.
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
You should not do this unless you’re really trying to take advantage of JavaScript’s runtime behavior in a clever way.
Computed and constant members
Each enum member has a value associated with it which can be either constant or computed. We will consider an enum member is considered constant if:
- It is the first member in the enum and it has no initializer, in which case it’s assigned the value
0
.
// E.X is constant:
enum E {
X,
}
// All enum members in 'E1' and 'E2' are constant.
enum E1 {X,
Y,
Z,
}
enum E2 {
A = 1,
B,
C,
}
- /
/ E.X is constant:enum E {X,}
- It does not have an initializer and the other enum member was a numeric constant. In this case, the value of the current enum member will be the value of the above enum member plus one.
// All enum members in 'E1' and 'E2' are constant.
enum E1 {X,
Y,
Z,
}
enum E2 {
A = 1,
B,
C,
}
- The enum member defines with a constant enum expression. It fully evaluates composing as it is a subset of TypeScript expressions. An expression is a constant enum expression if it is
- a literal enum expression (basically a string literal or a numeric literal)
- a reference to previously defined constant enum member (which can originate from a different enum)
- a parenthesized constant enum expression
- one of the +, -, ~ unary operators applied to constant enum expression
- +, -, *, /, %, <<, >>, >>>, &, |, ^ binary operators with constant enum expressions as operands is a compile-time error for constant enum expressions to be evaluated to
NaN
or Infinity
.
We consider enum number evaluated in all other cases.
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length,
}
Union enums and enum member types
We can’t calculate literal enum members as they are a special subset of constant enum members. A literal enum member is a constant enum member with no initialized value, or with values that are initialized to.
- any string literal (e.g.
"foo"
, "bar
, "baz"
) - any numeric literal (e.g.
1
, 100
) - a unary minus applied to any numeric literal (e.g.
-1
, -100
)
When all members in an enum have literal enum values, some special semantics come to play.
The first is that enum members also become types as well! For example, we can say that certain members can only have the value of an enum member.
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
radius: 100,
};
The other change is that enum types themselves definitely become a union of each enum member. With union enums, the type system is able to hold the fact that it knows the exact set of values that exist in the enum itself. Because of that, TypeScript can catch bugs where we might be comparing values incorrectly. For example:
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.
//
}
}
In that example, we first checked whether x
was not E.Foo
. If that check succeeds, then our ||
will short-circuit, and the body of the ‘if’ will run. However, if the check didn’t succeed, then x
can only be E.Foo
, so it doesn’t make sense to see whether it’s equal to E.Bar
.
Enums at runtime
Enums are real objects that exist at runtime. For example, the following enum.
can actually be passed around to functions
enum E {
X,
Y,
Z,
}
function f(obj: { X: number }) {
return obj.X;
}
// Works, since 'E' has a property named 'X' which is a number.
f(E);
Enums at compile time
Even though Enums are real objects that exist at runtime, the keyword works differently than you might expect for typical objects. Instead, use keyof typeof
to get a Type that represents all Enum keys as strings.
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
/**
* This is equivalent to:
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
function printImportant(key: LogLevelStrings, message: string) {
const num = LogLevel[key];
if (num <= LogLevel.WARN) {
console.log("Log level key is:", key);
console.log("Log level value is:", num);
console.log("Log level message is:", message);
}
}
printImportant("ERROR", "This is a message");
Reverse mappings
To create an object with property names for members, numeric enum members also get a reverse mapping from enum values to enum names. For example, in this example
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
TypeScript composes this down to the following JavaScript.
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
In this generated code, an enum is composed into an object that stores both forward (name
-> value
) and reverse (value
-> name
) mappings. Enum members are always released as property accesses and never inlined as compare to others.
You must remember that string enum members do not get a reverse mapping generated at all.
const
enums
In most cases, enums are a perfectly valid solution. However, sometimes conditions are more fixed. We use constenums to avoid handling the cost of extra generated code and other indirection when obtaining enum values. Const enums are defined using the const
modifier on our enums.
const enum Enum {
A = 1,
B = A * 2,
}
Const enums can only use constant enum expressions and unlike regular enums, they are completely removed during collection. These members are inlined at use sites. This is possible since const enums cannot have calculated members.
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
in generated code will become
<code>"use strict";let directions = [ 0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */,];</code>
Ambient enums
We use ambient enums to define the shape of already existing enum types.
<code>declare enum Enum { A = 1, B, C = 2,}</code><a href="https://www.typescriptlang.org/play/#code/CYUwxgNghgTiAEIB2BXAtvAoqjBvAUPPAILwC88AjADSHwBCtRAwufAEy0C+QA">Try</a>
One important difference between ambient and non-ambient enums is that, in regular enums, members that don’t have an initializer will be considered constant if their preceding enum member is considered constant. In comparison, an ambient (and non-const) enum member that does not have an initializer is always considered computed.
Objects vs Enums
In modern TypeScript, you may not need an enum when an object with as const
could suffice.
<code>const enum EDirection { Up, Down, Left, Right,}
const ODirection = { Up: 0, Down: 1, Left: 2, Right: 3,} as const;
EDirection.Up;// ^ = (enum member) EDirection.Up = 0
ODirection.Up;// ^ = (property) Up: 0
// Using the enum as a parameterfunction walk(dir: EDirection) {}
// It requires an extra line to pull out the keystype Direction = typeof ODirection[keyof typeof ODirection];function run(dir: Direction) {}
walk(EDirection.Left);run(ODirection.Right);</code><a href="https://www.typescriptlang.org/play/#code/MYewdgzgLgBApmArgWxgUQCIEsBOdhRbgwDeAUDDAKoAOANBTBiAO5gOUAycAZlBzABKWAOYALfmQC+ZMqEiwA8tjwEiYGAF5SjWgC4YABgHM2BgIwDufAwCYBw8VAMBmBlJgBDCDHnQA3LKYuPiE4AB0tIEA9NGU8QkAegD8ssohahFRZLEJeSmyuVQQWGAiMFBicPBIqN5eMDSeOJ7IcFBwOGQ8iGCZGiyeADYA1gAUACa4BsGqYWAAlKQyOXEAkrB4AI6IIT6eGnAAHlAtMEOl1VAgjYhDQzAgiLCV1SNwAJ4QZFAfNNUqULqLQVP5wEA8GDpObqADa7w+ENB-yR0KB4AAuoEen15jAcL1JtMmBl5ksSCtBqMxrN0WBwtYoAtAgSwGM0f1wo4JMygA">Try</a>
You can keep your codebase followed by the state of JavaScript, and when/if enums are added to JavaScript then you can move to the additional syntax. Therefore, it is the most significant argument in favor of this format.
- 4 years ago
- Afaq Arif
- 2,744 Views
-
2