Enums
Name a fixed set of choices with a TypeScript enum, see the JavaScript it turns into, and learn when a literal union does the same job with no extra code.
- 01Write an enumMake numeric and string enums, predict each member's value, and read the errors TypeScript gives for a wrong value.
- 02Know what it becomesRead the JavaScript tsc writes for an enum and a const enum, including the reverse mapping from numbers to names.
- 03Pick enum or unionReplace an enum with a literal union or an
as constobject, and say when each one fits better.
Names for a fixed set of choices
An order on a shop page is pending, shipped, or delivered. Many apps store that as a number, because numbers are small and quick to compare. Here is what that looks like in plain JavaScript:
const order = { id: 7, status: 1 };
if (order.status === 1) {
console.log("Your order is on its way");
}Line 1 makes order 7 with a status of 1. Line 3 checks whether the status is 1, and line 4 prints Your order is on its way. It works, but what does 1 mean? You have to remember, or find a comment somewhere, that 1 means "shipped". Type a 2 by mistake and nothing tells you.
TypeScript has a feature for this, called an enum (short for "enumeration", a list of every option). An enum gives names to a fixed set of values. Each name in it is a member:
enum OrderStatus { Pending, Shipped, Delivered,} const order = { id: 7, status: OrderStatus.Shipped }; if (order.status === OrderStatus.Shipped) { console.log("Your order is on its way");}console.log(order.status);Lines 1 to 5 make an enum called OrderStatus with three members: Pending, Shipped, and Delivered. You read a member like a property, with a dot: line 7 gives the order the status OrderStatus.Shipped, and line 9 compares with it. Line 10 prints Your order is on its way.
Line 12 shows what the status really is: it prints 1. You didn't write any numbers. TypeScript numbered the members for you, counting from 0, so Pending is 0, Shipped is 1, and Delivered is 2. Your code reads like words, and still stores the small number.
OrderStatus.Shipped. When the code runs, each name stands for a number, counting up from 0.An enum is also a type. status: OrderStatus means "one of the three statuses". Try it: type a value for line 7 and watch the list under the code.
enum OrderStatus { Pending, Shipped, Delivered,} const status: OrderStatus = OrderStatus.Shipped;console.log(status);OrderStatus.ShippedNo errorsprints 1OrderStatus.Shipped is a member of the enum, so it fits. Its value is the number 1, because members count up from 0.
OrderStatus.Delivered fits and prints 2. A typo like OrderStatus.Shiped gets an error that suggests the right name. The text "Shipped" gets an error, because a name in quotes isn't the member. The surprise is 1: no error, because some member has the value 1. 7 gets an error, because no member does.
A washing machine has a few programs. Inside, its computer knows them as program 0, 1, and 2. But the buttons on the front say Cotton, Wool, and Quick, so you never have to remember which number is which. You press Wool, and the machine gets 1. There's no button for a program the machine doesn't have.
- In real life: The machine has a fixed set of programs
- In TypeScript: An enum has a fixed set of members
- In real life: The buttons say Cotton, Wool, Quick
- In TypeScript: You write
OrderStatus.Shipped - In real life: Inside, the machine runs program 0, 1, or 2
- In TypeScript: When the code runs,
Shippedis the number 1 - In real life: There's no button for a program that doesn't exist
- In TypeScript: A member that doesn't exist gets an error
An enum gives names to a fixed set of values. You write OrderStatus.Shipped, and when the code runs that name stands for a number, like 1, or a string. Unlike a type alias, an enum doesn't disappear: TypeScript turns it into a JavaScript object.
Numeric and string enums
An enum whose members are numbers is a numeric enum. If you give the first member a number, the rest count up from there:
enum Priority { Low = 1, Medium, High,} let level = Priority.Medium;// ^?console.log(level, Priority.High);Line 2 says Low = 1. Lines 3 and 4 have no number, so Medium is 2 and High is 3. Line 7 stores the medium priority, and the query on line 8 shows let level: Priority: the variable can hold any of the three. Line 9 prints 2 3.
You saw that a numeric enum accepts the number 1. It goes further than that, and this is the weak spot of numeric enums:
enum Priority { Low = 1, Medium, High,} const typed: Priority = 2;const tooBig: Priority = 7; const fromServer: number = 7;const loose: Priority = fromServer;console.log(typed, tooBig, loose);Line 7 stores the number 2, which is Medium's value, so it's fine. Line 8 stores 7, and TypeScript marks it on purpose: Type '7' is not assignable to type 'Priority', "no member has the value 7". But line 10 makes a plain number, the way a value from a server would arrive, and line 11 stores it with no error. TypeScript checks a number you write in the code; any value whose type is just number gets in. Line 12 prints 2 7 7.
The other kind is a string enum. Each member gets a text value, written with =. There's no counting, so you write every value:
enum Theme { Light = "light", Dark = "dark",} function setTheme(theme: Theme) { console.log(`Switched to ${theme}`);} setTheme(Theme.Dark);setTheme("dark");Lines 1 to 4 make Theme, whose Light member is the text "light" and Dark member is "dark". Line 6 says setTheme takes a Theme. Line 10 passes Theme.Dark, which fits. Line 11 passes the text "dark", and TypeScript marks it on purpose: Argument of type '"dark"' is not assignable to parameter of type 'Theme'. The value is the same, but a string enum accepts only its members. Run it anyway, and both lines print Switched to dark.
String values are easier to read: a log or a saved setting says dark, not 1. The table below compares the two kinds. Read each row as a question, then the answer for each kind.
| Question | Numeric enum | String enum |
|---|---|---|
| How are the values set? | They count up from 0, or from the first number you give. | You give every member its text, like Dark = "dark". |
| What does a value look like when you log it? | A number, like 1, which says little on its own. | Its text, like dark, which you can read. |
Does a plain value fit, like 1 or "dark"? | A number you write fits if a member has it. Any number from a variable fits. | No. Only the members fit, like Theme.Dark. |
| Can you look up a name from a value? | Yes: OrderStatus[1] is "Shipped". | No. A string enum has no reverse mapping. |
What an enum compiles to
Most of TypeScript is types, and types are removed before the code runs. An enum is different: your code reads OrderStatus.Shipped while it runs, so something called OrderStatus has to exist. Here is a small enum:
enum OrderStatus { Pending, Shipped, Delivered,} console.log(OrderStatus.Shipped);console.log(OrderStatus[1]);Line 7 reads a member by name and prints 1. Line 8 reads with a number in square brackets and prints Shipped. That second read only works because of the JavaScript the TypeScript compiler, tsc, writes for the enum:
var OrderStatus;
(function (OrderStatus) {
OrderStatus[OrderStatus["Pending"] = 0] = "Pending";
OrderStatus[OrderStatus["Shipped"] = 1] = "Shipped";
OrderStatus[OrderStatus["Delivered"] = 2] = "Delivered";
})(OrderStatus || (OrderStatus = {}));
console.log(OrderStatus.Shipped);
console.log(OrderStatus[1]);In plain words, line by line:
- Line 1 makes a variable,
OrderStatus, with no value yet. - Lines 2 to 6 are a function that runs as soon as it's made. Line 6 calls it, with
OrderStatus || (OrderStatus = {}): if the variable is empty, it first gets a new empty object (the Logical operators lesson covers||). - Line 3 does two stores in one. The inner part,
OrderStatus["Pending"] = 0, stores the number 0 under the name Pending. An assignment gives back the value it stored, 0, so the outer part becomesOrderStatus[0] = "Pending": the name stored under the number. - Lines 4 and 5 do the same for Shipped (1) and Delivered (2).
- Lines 7 and 8 are your two reads, unchanged.
Step through it and watch the object fill up:
This replays the JavaScript tsc writes for the enum, one assignment at a time. Watch the object fill up with six entries: three names and three numbers.
script
(function (OrderStatus) { OrderStatus[OrderStatus["Pending"] = 0] = "Pending"; OrderStatus[OrderStatus["Shipped"] = 1] = "Shipped"; OrderStatus[OrderStatus["Delivered"] = 2] = "Delivered";})(OrderStatus || (OrderStatus = {}));console.log(OrderStatus.Shipped);console.log(OrderStatus[1]);Each member leaves two entries: one from the name to the number, and one from the number back to the name. The second kind is called a reverse mapping. It's why OrderStatus[1] gives "Shipped", which helps when a number arrives and you want to show its name.
OrderStatus is an object with six entries. Each name leads to its number, and each number leads back to its name, so OrderStatus[1] is "Shipped".The reverse mapping has a side effect. The object has six keys, not three:
enum OrderStatus { Pending, Shipped, Delivered,} console.log(Object.keys(OrderStatus).join(", "));console.log(Object.keys(OrderStatus).length);Line 7 lists the object's keys with Object.keys and prints 0, 1, 2, Pending, Shipped, Delivered. Line 8 prints 6. If you build a menu from a numeric enum's keys, you'll get the numbers too. String enums don't have this problem:
enum Theme { Light = "light", Dark = "dark",} console.log(Theme.Dark);var Theme;
(function (Theme) {
Theme["Light"] = "light";
Theme["Dark"] = "dark";
})(Theme || (Theme = {}));
console.log(Theme.Dark);Lines 3 and 4 store only the name to the text, like Theme["Dark"] = "dark". A string enum has no reverse mapping, so Object.keys(Theme) lists just Light and Dark. Line 6 prints dark.
const enums leave no object
Write const before enum and you get a const enum. It works the same in your code, but tsc doesn't write an object for it. Instead it copies each member's value to every place that uses it:
const enum Size { Small = "S", Large = "L",} const pick = Size.Large;console.log(pick);const pick = "L" /* Size.Large */;
console.log(pick);In the TypeScript, lines 1 to 4 make Size, and line 6 reads Size.Large. In the JavaScript, there's no Size at all. Line 1 holds the value "L" itself, with a comment saying which member it came from. This is called inlining. Line 2 prints L.
"L" to the place that used Size.Large, and writes nothing for the enum itself. No Size object exists when the code runs.Because there's no object, you can only read members of a const enum, never use the enum itself as a value:
const enum Size { Small = "S", Large = "L",} console.log(Object.values(Size).join(", "));Line 6 passes Size to Object.values, and TypeScript marks it on purpose: 'const' enums can only be used in property or index access expressions …. In plain words: you may write Size.Large or Size["Large"], and nothing else, because there will be no Size to pass.
One more thing to know. Inlining needs the type checker, because tsc must know each member's value. Many build tools turn TypeScript into JavaScript one file at a time, without checking types, and so does this page's editor. They keep a const enum as an ordinary object instead. Your code works the same either way, but don't count on the object being gone.
Literal unions instead
Many teams don't use enums at all. A union of exact texts does most of the same job, with no JavaScript left over. Each option is a literal type: a type that allows one exact value. Here is a search page's sort menu:
type SortOrder = "relevance" | "price-low" | "price-high"; function sortLabel(order: SortOrder) { return `Sorted by ${order}`;} console.log(sortLabel("price-low"));console.log(sortLabel("cheapest"));Line 1 says a SortOrder is one of three exact texts. Line 3 says sortLabel takes one. Line 7 passes "price-low", which fits, and prints Sorted by price-low. Line 8 passes "cheapest", which isn't an option, and TypeScript marks it on purpose: Argument of type '"cheapest"' is not assignable to parameter of type 'SortOrder'. So you get the same safety as an enum: only the listed options fit, and the editor suggests them when you type a quote.
What you don't get is names like SortOrder.PriceLow. If you want them, write a plain object with as const, which keeps every value exact and read-only:
const SortOrder = { Relevance: "relevance", PriceLow: "price-low", PriceHigh: "price-high",} as const; type SortOrder = (typeof SortOrder)[keyof typeof SortOrder];// ^? function sortLabel(order: SortOrder) { return `Sorted by ${order}`;} console.log(sortLabel(SortOrder.PriceLow));console.log(sortLabel("price-high"));Lines 1 to 5 are an ordinary JavaScript object with three names and three texts. Line 7 makes the type, and reads from the inside out:
typeof SortOrderis the type of the object on line 1.keyof typeof SortOrderis its names:"Relevance" | "PriceLow" | "PriceHigh".- The square brackets look up the values at those names, which gives the union of the three texts.
The query on line 8 shows the result: type SortOrder = "relevance" | "price-low" | "price-high". The object and the type can share the name SortOrder, because one is a value and the other is a type. Line 14 uses a name, line 15 uses plain text, and both fit. They print Sorted by price-low and Sorted by price-high. (The keyof & typeof lesson explains line 7 in full.)
Compare the three ways side by side. Each button shows the same sort menu:
enum SortOrder { Relevance = "relevance", PriceLow = "price-low", PriceHigh = "price-high",} function sortLabel(order: SortOrder) { return `Sorted by ${order}`;} console.log(sortLabel(SortOrder.PriceLow));console.log(sortLabel("price-high"));Sorted by price-low, Sorted by price-high1 error: line 12Noa SortOrder objectLine 11 passes the member SortOrder.PriceLow, which fits. Line 12 passes the text "price-high", and a string enum doesn't accept it: Argument of type '"price-high"' is not assignable to parameter of type 'SortOrder'. tsc also writes a SortOrder object into the JavaScript.
All three print the same thing. They differ in two places: whether plain text like "price-high" fits, and what tsc leaves in the JavaScript.
as const object stays because it's ordinary JavaScript you wrote.That last difference matters more than it used to. Node.js can now run a .ts file by only removing its types, and some build tools work the same way. An enum can't run there, because it's code that has to be written, not a type that can be removed. TypeScript 5.8 added a setting, erasableSyntaxOnly, that reports anything like this:
enum Theme { Light = "light", Dark = "dark",} type ThemeName = "light" | "dark";const saved: ThemeName = "dark";console.log(Theme.Dark, saved);The list under the code shows the setting this example turns on. Line 1 gets This syntax is not allowed when 'erasableSyntaxOnly' is enabled, on purpose. Line 6, a literal union, is only a type, so it's fine. Line 8 prints dark dark.
The table below sums up the choice. Read each row as a question, then the answer for each way.
| Question | Enum | Literal union |
|---|---|---|
| Is anything left in the JavaScript? | Yes, an object, unless it's a const enum. | No. The type is removed. |
Does plain text like "price-high" fit? | Not for a string enum. You write SortOrder.PriceHigh. | Yes, if it's one of the options. |
| Does the editor suggest the options? | Yes, after you type SortOrder.. | Yes, when you type a quote. |
| Can the code list every option while it runs? | Yes, with Object.values, but a numeric enum lists its numbers too. | Not from the type alone. Keep the options in an as const object if you need the list. |
Does it run where types are only removed, like Node.js running a .ts file? | No. An enum is code that tsc has to write. | Yes. It's only a type. |
A simple rule: reach for a literal union first, and an as const object when you also want names or a list of the options. Use an enum when your project already does, or when you want a numeric code with readable names, and know that it becomes an object.
A status badge from a server reply
An order page gets the order's status from a server, as text, and shows a badge for it. A string enum fits well here: its values match the server's text, and the code reads OrderStatus.Shipped. The careful part is turning the server's text into an OrderStatus:
enum OrderStatus { Pending = "pending", Shipped = "shipped", Delivered = "delivered",} function badge(status: OrderStatus) { switch (status) { case OrderStatus.Pending: return "Waiting to ship"; case OrderStatus.Shipped: return "On its way"; case OrderStatus.Delivered: return "Delivered"; }} function toStatus(text: string) { return Object.values(OrderStatus).find((status) => status === text);} const reply = JSON.parse('{ "id": 7, "status": "shipped" }');const status = toStatus(reply.status);// ^?console.log(status ? badge(status) : "Unknown status");console.log(toStatus("lost") ?? "Unknown status");Lines 1 to 5 make the enum, with the same texts the server sends. Lines 7 to 16 choose the badge text with a switch, one case per member. Lines 18 to 20 turn text into a status: Object.values(OrderStatus) lists the three texts, and find gives back the one that matches, or undefined if none does.
Line 22 reads the server's reply with JSON.parse. Line 23 turns its status into an OrderStatus, and the query on line 24 shows const status: OrderStatus | undefined: TypeScript knows the text might not match. Line 25 checks before it calls badge, and prints On its way. Line 26 tries the text "lost" and prints Unknown status.
Why not pass reply.status straight to badge? JSON.parse gives back any, the type TypeScript doesn't check (see any, unknown & never), so it would let anything through, even "lost". Checking the text against the enum's values first means badge only ever gets a real status.
enum Size { Small, Large } const s: Size = Size.Large;enum Size { Small, Large } const s: Size = 5;enum Theme { Dark = "dark" } const t: Theme = "dark";enum Theme { Dark = "dark" } const t: Theme = Theme.Dark;type Theme = "light" | "dark"; const t: Theme = "dark";const enum Size { Small, Large } console.log(Object.keys(Size));enum Size { Small, Large } console.log(Size[0]);enum Theme { Dark = "dark" } console.log(Theme.Drak);
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
- "An enum is only a type, like a type alias." No. tsc writes an object for it, and your code reads that object while it runs. Only a const enum is removed, and only when tsc compiles it.
- "A numeric enum only accepts its members." No. A number you write is checked, but any value of type
numbergets in, soPrioritycan end up holding 7. - "A string enum accepts its text." No.
"dark"doesn't fit aTheme, even thoughTheme.Darkis"dark". A literal union accepts it. - "Object.keys gives an enum's names." Only for a string enum. A numeric enum also lists its numbers, because of the reverse mapping.
- "Members are numbered from 1." No. They start at 0, unless you give the first one a number.
- "An enum checks the server's text for me." No. Like every type, it can't see data that arrives while the page runs. Compare the text with
Object.values, as the badge code does.
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code to try your answer: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
A checkout has four steps. What does line 8 print?
enum Step { Cart, Address, Payment, Done,} console.log(Step.Payment);enum Step { Cart, // 0 Address, // 1 Payment, // 2 Done, // 3} console.log(Step.Payment);Members count up from 0: Cart is 0, Address is 1, Payment is 2, and Done is 3. So Step.Payment prints 2.
Two saved themes, one with a mistake. Which line will TypeScript mark?
enum Theme {
Light = "light",
Dark = "dark",
}
const saved: Theme = Theme.Light;
const chosen: Theme = "dark";enum Theme { Light = "light", Dark = "dark",} const saved: Theme = Theme.Light;const chosen: Theme = Theme.Dark; // line 7 needed the member, not the textLine 7 gives a Theme the text "dark". A string enum accepts only its members, so TypeScript reports Type '"dark"' is not assignable to type 'Theme'. Theme.Dark fixes it.
The priorities should be numbered 1, 2, 3, to match the server. Change one line so they are. What does the code print now?
enum Priority { Low, Medium, High,} console.log(Priority.High);enum Priority { Low = 1, Medium, High,} console.log(Priority.High);With Low = 1, Medium is 2 and High is 3, so the code prints 3. The starter printed 2, because counting started at 0.
A server sends the status as the number 2. What does line 8 print?
enum OrderStatus { Pending, Shipped, Delivered,} const fromServer = 2;console.log(OrderStatus[fromServer]);enum OrderStatus { Pending, // 0 Shipped, // 1 Delivered, // 2} const fromServer = 2;console.log(OrderStatus[fromServer]); // the name stored for 2Delivered is the third member, so its value is 2. The reverse mapping stores OrderStatus[2] = "Delivered", so the code prints Delivered.
Rewrite Theme as a literal union and fix the call. How many lines of JavaScript does your Theme leave behind?
enum Theme { Light = "light", Dark = "dark",} function setTheme(theme: Theme) { console.log(`Switched to ${theme}`);} setTheme(Theme.Dark);type Theme = "light" | "dark"; function setTheme(theme: Theme) { console.log(`Switched to ${theme}`);} setTheme("dark");A literal union is only a type, so tsc removes it and writes no lines for Theme. The call passes "dark", which fits, and the code still prints Switched to dark.
TypeScript marks line 12: a plain string isn't an OrderStatus. Turn the text into a status before you call badge. What does your fixed code print?
enum OrderStatus { Pending = "pending", Shipped = "shipped",} function badge(status: OrderStatus) { return status === OrderStatus.Shipped ? "On its way" : "Waiting";} const reply = JSON.parse('{ "status": "shipped" }');const text: string = reply.status;console.log(badge(text));enum OrderStatus { Pending = "pending", Shipped = "shipped",} function badge(status: OrderStatus) { return status === OrderStatus.Shipped ? "On its way" : "Waiting";} const reply = JSON.parse('{ "status": "shipped" }');const text: string = reply.status;const status = Object.values(OrderStatus).find((value) => value === text);console.log(status ? badge(status) : "Unknown status");find gives back the member whose value matches the text, typed as OrderStatus | undefined. Inside the check, it's a real OrderStatus, so the error is gone, and the code prints On its way. Any other text would print Unknown status.
Check your understanding
For each question, ask two things: what values the enum has, and what tsc writes for it. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
Color.Blueprint?Read the code, then predictenum Color { Red, Green, Blue } console.log(Color.Blue);Choose an answer to see the explanation.
Question 2 of 7Which of these is still there when the code runs?
Choose an answer to see the explanation.
Question 3 of 7What does TypeScript say about line 2 of this theme code?
Read the code, then predictenum Theme { Light = "light", Dark = "dark" } const theme: Theme = "dark";Choose an answer to see the explanation.
Question 4 of 7What does this enum, which starts at 5, print?
Read the code, then predictenum Level { Low = 5, High } console.log(Level.High, Level[6]);Choose an answer to see the explanation.
Question 5 of 7Does TypeScript accept a number from a variable here, and what prints?
Read the code, then predictenum Level { Low, High } const fromServer: number = 9; const level: Level = fromServer; console.log(level);Choose an answer to see the explanation.
Question 6 of 7What does tsc write for line 6 of this const enum?
Read the code, then predictconst enum Size { Small = "S", Large = "L", } const pick = Size.Large;Choose an answer to see the explanation.
Question 7 of 7Why might a team write a literal union instead of an enum?
Choose an answer to see the explanation.
Key takeaways
- An enum gives names to a fixed set of values:
OrderStatus.Shippedinstead of a bare1. - Numeric enums count up from 0, or from the first number you give. String enums give every member its text.
- A numeric enum accepts any
numberfrom a variable; a string enum accepts only its members, not their text. - tsc turns an enum into an object. A numeric enum also maps numbers back to names, so
Object.keyslists both. - A const enum is inlined by tsc and leaves no object, so you can only read its members.
- A literal union, or an
as constobject, does the same job with no extra code and runs where types are only removed.
Remember the one-liner.
An enum is a named set of values that becomes a real object; a literal union names the same choices and disappears.
Coming next: Typing functions. It shows how to type a function's parameters and the value it gives back, like the badge function on this page.