Skip to content

Literal Types and Enums

A string variable can hold any string. But AceIt’s difficulty can only be 'easy', 'medium', or 'hard'. TypeScript has two tools for expressing this constraint: literal types and enums.

A literal type is a type that represents exactly one value. Instead of string, you write the specific string itself:

const difficulty: 'easy' = 'easy'; // can only ever be 'easy'

On its own this is not useful — but combined with a union, it describes a fixed set of allowed values:

type Difficulty = 'easy' | 'medium' | 'hard';
let level: Difficulty = 'easy'; // ✓
level = 'medium'; // ✓
level = 'expert'; // Error: Type '"expert"' is not assignable to type 'Difficulty'

Now TypeScript enforces that only valid difficulty values are used anywhere Difficulty appears. A typo ('medum') is a compile error rather than a silent mismatch that breaks the API request.

TypeScript widens let variables initialized with a literal to their base type:

let d = 'easy'; // inferred: string, not 'easy'

TypeScript assumes a let variable will be reassigned, so it infers string — the widest type that includes 'easy'. If you want a const literal:

const d = 'easy'; // inferred: 'easy' (literal type)

const cannot be reassigned, so TypeScript infers the exact literal. This distinction matters when passing values to functions that expect a Difficulty:

let level = 'easy'; // inferred: string
fetchQuestions(10, level); // Error: 'string' is not assignable to 'Difficulty'
const level = 'easy'; // inferred: 'easy'
fetchQuestions(10, level); // ✓

If you need a mutable variable constrained to a literal union, annotate it explicitly:

let level: Difficulty = 'easy'; // annotated — not widened
level = 'hard'; // ✓

An enum defines a named set of constants. Unlike a union of literals, enum members have a name and a value — by default numeric, but string enums are common in real code:

enum ResponseCode {
Success = 0,
NoResults = 1,
InvalidParam = 2,
TokenNotFound = 3,
TokenEmpty = 4,
}

You refer to members by name rather than by value:

if (data.response_code === ResponseCode.Success) {
// process results
}
if (data.response_code === ResponseCode.NoResults) {
throw new Error('Not enough questions available');
}

Using ResponseCode.Success instead of the magic number 0 makes the code self-documenting. If the API ever changes a code, you update the enum in one place.

AceIt’s types.ts defines both a literal union and an enum:

// Literal union — for values that appear as strings in the UI and API
export type Difficulty = 'easy' | 'medium' | 'hard';
// Enum — for the API's numeric response codes
export enum ResponseCode {
Success = 0,
NoResults = 1,
InvalidParam = 2,
TokenNotFound = 3,
TokenEmpty = 4,
}

Difficulty stays as a string literal union because it appears directly in the URL (&difficulty=easy) — the actual string value matters. ResponseCode is an enum because the raw numbers (0, 1, 2) have no meaning without names, and the enum communicates intent clearly.

Use a literal union when:

  • The values are strings that appear directly in code or APIs
  • You want to keep it simple — no import needed, works in type position only
  • The set is small and unlikely to grow

Use an enum when:

  • The values are numeric codes with no inherent meaning
  • Members need descriptive names that the raw values do not provide
  • You want a runtime object (enums exist in the compiled JS; literal unions do not)

In types.ts in your aceit/ folder:

  1. Define type Difficulty = 'easy' | 'medium' | 'hard'. Write a function buildDifficultyParam(d: Difficulty): string that returns &difficulty=${d}.
  2. Define enum ResponseCode with the five values above.
  3. Write a function handleResponseCode(code: ResponseCode): void that logs a descriptive message for each code using a switch statement.
  4. Declare let level = 'easy' and try passing it to buildDifficultyParam. Read the error. Fix it two ways: with const, and with an explicit Difficulty annotation.
  • A literal type represents exactly one value — combined with |, it describes a fixed set of options.
  • let variables initialized with a literal are widened to the base type; const preserves the literal.
  • Enums define named constants with values — numeric by default, but string enums are common.
  • Use literal unions for string values that appear directly in code or APIs.
  • Use enums for numeric codes or constants that need descriptive names to be readable.