Chapter 9 of 24 · TypeScript for Beginners — Complete Free Tutorial

Literal Types and Enums

You already narrowed values to 'guest' or 'admin' in the previous chapter of the TypeScript for Beginners series. This chapter of the 24-chapter series makes those fixed values a first-class tool: literal types, unions of literals, the as const keyword, and the older enum syntax — plus honest guidance on which to prefer.

Literal types

A literal type stands for exactly one value. Every primitive has them, and boolean is nothing more than the union of its two literals:

let version: '5.9' = '5.9';   // only the string '5.9'
let retries: 3 = 3;           // only the number 3
let enabled: true = true;     // boolean is `true | false`

version = '6.0';              // ❌ cannot compile

Expected output: the last line fails with Type '"6.0"' is not assignable to type '"5.9"'. Widening explains how you meet these types: const bindings already infer the literal (const v = 'up' has type 'up'), while let v = 'up' widens to string because the variable may be reassigned.

Unions of literals: the modern default

One literal is rarely useful alone — a handful of them is a vocabulary. A union of literal types describes a fixed menu of values, and any typo is a compile-time error rather than a mystery at runtime:

type Theme = 'light' | 'dark';
type Size = 'sm' | 'md' | 'lg';
type HttpStatus = 200 | 201 | 404 | 500;

function apply(theme: Theme): string {
  return `theme set to ${theme}`;
}

console.log(apply('dark'));
console.log(apply('light'));

Expected output:

theme set to dark
theme set to light

These values are ordinary strings and numbers in the compiled output: nothing is imported, nothing is bundled, and JSON from an API matches them directly. That is why most TypeScript code written today reaches for a union of literals before it reaches for an enum.

const assertions

Arrays and objects widen too — ['/', '/about'] infers as string[], which admits '/anything'. A const assertion as const pins the whole value down, turning it into a readonly structure of literal types:

const routes = ['/', '/about', '/practice'] as const;
const defaults = { theme: 'dark', retries: 3 } as const;

console.log(routes[1]);
console.log(defaults.theme);
routes[0] = '/home';   // ❌ cannot compile

Expected output:

/about
dark

Without as const you would get string[] and { theme: string; retries: number }. With it, routes is a readonly tuple of exactly three literals (so reassignment fails with a read-only error) and defaults.theme is 'dark'. Use it on configuration objects, route lists and any value that must never drift from its original wording.

Numeric enums and reverse mapping

An enum (short for enumeration) declares a named set of values in one statement. Numeric enums are the original form: members are numbered from zero unless you say otherwise, and the compiler emits a real JavaScript object for them.

enum Direction { Up, Down, Left, Right }

console.log(Direction.Up);
console.log(Direction[0]);
console.log(Object.keys(Direction).join(', '));

let dir: Direction = Direction.Left;
console.log(dir);

Expected output:

0
Up
0, 1, 2, 3, Up, Down, Left, Right
2

Three properties are worth memorising. Members auto-number from 0, so Direction.Up is 0 and Direction.Left is 2. Numeric enums carry a reverse mapping: Direction[0] resolves back to 'Up', which is why Object.keys lists both the numbers and the names. And the type is strict in TypeScript 5.x — let dir: Direction = 7; fails with Type '7' is not assignable to type 'Direction', so stray numbers cannot sneak in.

String enums and const enum

String enums give every member an explicit string value, which keeps logs and network payloads readable instead of showing bare numbers:

enum ThemeMode { Light = 'LIGHT', Dark = 'DARK' }

function applyMode(mode: ThemeMode): string {
  return `mode: ${mode.toLowerCase()}`;
}

console.log(applyMode(ThemeMode.Dark));

Expected output:

mode: dark

String enums have no reverse mapping — ThemeMode['LIGHT'] is rejected with Property 'LIGHT' does not exist on type 'typeof ThemeMode' — and the values are nominally sealed: const m: ThemeMode = 'DARK' also fails, because a plain string is not the enum. Both rules are deliberate: the enum type only accepts members you declared.

The const enum variant exists to avoid the runtime object entirely. In const enum Flag { On = 1, Off = 0 } the compiler replaces Flag.On with the literal 1 at build time and emits nothing. That saves a few bytes, but there is no object to inspect at runtime, debugging shows raw numbers, and every transpiler must repeat the inlining — friction that single-file transpilers such as Vite's esbuild do not need. Most modern projects simply avoid const enum. Note also that TypeScript 5.8's erasableSyntaxOnly option rejects enum declarations outright (error TS1294), because enums are not erasable syntax — another nudge towards literal unions.

Enum or union? Plus declaration merging

Both tools solve the same problem, so choose deliberately:

ConcernUnion of literalsenum
Runtime costNone — values stay plain stringsA JavaScript object is emitted
TyposCaught at compile timeCaught at compile time
JSON round-tripJust worksNumeric enums need conversion
Listing the membersRequires your own arrayReverse mapping (numeric only)
Tooling frictionNoneconst enum and erasableSyntaxOnly complain

The default advice for new code is a union of literals; keep enum for existing codebases that already use it or for the rare case where you genuinely iterate over the members. One quirk you may meet in older libraries: enum declarations merge. Two blocks with the same name combine into a single enum, with the restriction that only one declaration can omit an initializer for its first enum element — otherwise TypeScript reports TS2432.

enum Palette { Red = 1 }
enum Palette { Blue = 2 }

console.log(Palette.Red, Palette.Blue);

Expected output:

1 2

Common mistakes

  • Letting variables widen when you did not mean it — let route = '/' becomes string; annotate let route: '/' = '/' or use as const.
  • Treating an enum as a string — 'DARK' is not assignable to ThemeMode; go through the member, or switch to a literal union.
  • Expecting reverse mapping from string enums — only numeric enums map backwards, and the object you get back is a runtime cost either way.
  • Freezing a value with as const that still needs to change — readonly blocks reassignment, so keep it for genuine constants such as config and routes.
  • Using const enum across files — inlining and single-file transpilation do not mix well; prefer a plain union or a regular enum.

Practice checklist

  1. Declare a union of four status strings and write a function that accepts only them.
  2. Convert one numeric enum from an old project into a literal union and delete the enum.
  3. Build an as const config object and read a property — then try to reassign it and note the error.
  4. Create a numeric enum, print Object.keys, and compare the output with the array form.
  5. Write a string enum, attempt const m: MyEnum = 'raw', and read the compiler message.

What's next?

Literal types describe precise values — next you meet the three special types that describe everything else, nothing, and whatever comes back: any, unknown and never.

Quiz: Literal Types and Enums

5 questions · 70% to pass

Frequently Asked Questions

TypeScript is a strict superset of JavaScript that adds a static type system. You annotate the promises your data makes, the compiler checks every one before the code runs, then erases the types and emits ordinary JavaScript. You catch wrong arguments, typoed properties and forgotten null checks while you are still typing, and refactoring plus editor autocomplete both improve sharply.

You should. This series assumes you can already read and write JavaScript: functions, arrays and objects, closures, modules and async/await. TypeScript audits that knowledge rather than replacing it, and types cannot rescue code you do not understand. If those fundamentals feel shaky, work through the JavaScript for Beginners series on this site first, and this course will go much faster.

No. TypeScript is compiled, not interpreted: tsc parses your file, type-checks it, then erases every annotation and writes ordinary JavaScript. The browser and Node.js only ever run that emitted .js, so runtime behaviour, performance and debugging are unchanged. It also never validates API data at runtime, so the series teaches you to distrust every boundary that leaves the compiler.

If you already know JavaScript, TypeScript is a modest step rather than a new language: the syntax is the same plus a colon. Most beginners read their first red squiggle within days and feel comfortable in two to four weeks of regular practice. The curve steepens later around generics and advanced types, which is where this 24-chapter series deliberately spends its second half.

Both name the same object shapes, so either works for a simple contract. The differences decide the rest: only interfaces merge when declared twice, while only type aliases can express unions, primitives, tuples and mapped types. The series default is interface for props and exported shapes other files consume, and type for everything else. Pick one style per project and stay consistent.

strict: true is a bundle of safety switches, not a single behaviour. It turns on noImplicitAny, so every parameter must be inferrable, strictNullChecks, so null and undefined are their own types you must narrow, strictFunctionTypes, noImplicitThis, strictPropertyInitialization, and useUnknownInCatchVariables, which types catch values as unknown. Every example in this series compiles with it on.

any accepts anything and switches the compiler off, so one any spreads through inference into everything derived from it and you are back to dynamic typing with a .ts extension. unknown accepts anything too, but forbids property access until you narrow it with a typeof or instanceof check. The series pushes you toward unknown because it keeps the check you installed TypeScript to get.

Yes to both. Vite understands TypeScript natively: scaffold the vanilla-ts or react-ts template and a tsconfig.json is waiting for you, with moduleResolution set for the bundler. JSX belongs in .tsx files, props get an interface, and hooks infer or accept explicit type arguments. The series covers typed React in depth, from props, state and events to refs, context and fetch effects.

Enums still compile and work fine in TypeScript 5.x, but they are no longer the default recommendation. A union of string literals such as "light" | "dark" needs no runtime object, matches JSON directly and plays well with single-file transpilers like Vite's esbuild. Keep enum for existing codebases that already use it, avoid const enum, and prefer literal unions in new code.

Adopt it incrementally instead of rewriting. Install typescript, add a tsconfig.json with allowJs and noEmit so .js and .ts coexist while your bundler keeps building, then check individual files with a // @ts-check comment. Rename leaf modules to .ts one at a time, fix implicit any errors first, and gate every pull request with tsc --noEmit so untyped code cannot sneak back in.