Skip to content

MODELING DATA · LESSON 6 OF 24

Union types and explicit application states

Represent valid alternatives and handle every state.

  • union
  • literal
  • discriminant
  • never

Your runnable checkpoint

Extract into a new practice folder. Each download includes complete source, package.json, tsconfig.json and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.

Install with npm install. Run npm run check, then npm run build. Terminal lessons use npm start; browser lessons use npm run serve and http://127.0.0.1:4173.

1. Understand the concepts

A union says a value can be one of several alternatives. You may use only operations safe for every alternative until a check narrows the value. Literal types restrict a value to specific strings or numbers.

Discriminated unions give each object variant a common property with a distinct literal value. This avoids contradictory combinations such as loading=true together with an unrelated error message.

A switch over the discriminant narrows the whole object. The success branch can read data; the error branch can read message. Accessing those fields before narrowing is a compiler error.

An exhaustive never check makes a forgotten variant visible during development. If you add a new state, the default branch can no longer pass that value to a never parameter. The runtime throw also protects this path if unvalidated external data slips through.

2. Build and run

These are complete files. Preserve the paths shown below, including the src directory. The package and compiler configuration are included at the end.

src/main.ts

src/main.ts
type LoadState =
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; message: string };
function unreachable(value: never): never {
  throw new Error(`Unexpected state: ${JSON.stringify(value)}`);
}
function describe(state: LoadState): string {
  switch (state.status) {
    case "loading": return "Loading…";
    case "success": return `${state.data.length} lessons`;
    case "error": return state.message;
    default: return unreachable(state);
  }
}
console.log(describe({ status: "success", data: ["Types", "Functions"] }));
console.log(describe({ status: "error", message: "Try again" }));

package.json

package.json
{
  "name": "edupoly-typescript-unions-and-discriminated-unions",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "check": "tsc --noEmit",
    "build": "tsc",
    "start": "node dist/main.js"
  },
  "devDependencies": {
    "typescript": "5.9.3"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": [
      "ES2022",
      "DOM"
    ],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noEmitOnError": true,
    "verbatimModuleSyntax": true,
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true,
    "types": [],
    "moduleDetection": "force"
  },
  "include": [
    "src/**/*"
  ]
}

3. Trace the code and check the result

  1. status selects the union member.
  2. data exists only in the success member.
  3. never describes a branch with no remaining valid states.

Expected result

2 lessons
Try again

4. Practice independently

Add an idle state and make the compiler identify the missing handling.

Show the explanation

Add | { status: "idle" }, run npm run check, then add case "idle": return "Ready" to the switch.

When something goes wrong

If tsc is missing, run npm install in the folder containing package.json. If the output looks old, rebuild before running. Read the first compiler diagnostic and check the indicated field or argument. For module errors, preserve the .js extensions in imports. Browser examples must run through the included local server, not directly as TypeScript files.

Read the related official TypeScript documentation ↗