TypeScript · Course navigation

TypeScript: a process lifecycle as a typed state machine

Allow only legal transitions between process states and reject the rest.

On this page
Before you start

TypeScript union types and Record.

  • Allow only legal transitions between process states and reject the rest.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

An operating system tracks each process through states. A new process becomes ready, the scheduler dispatches it to running, a running process may be preempted back to ready, blocked on I/O in the waiting state, or finish as terminated. A waiting process becomes ready again when its I/O completes, but it cannot jump straight to running.

A string-literal union type lists the legal states, so a typo in a state name is a compile error. The Record<State, State[]> table gives the legal next states for each state, and the compiler insists that every state has an entry. The move function enforces the table at run time and throws on an illegal transition.

Code example

TypeScript
type State = "new" | "ready" | "running" | "waiting" | "terminated";

const transitions: Record<State, State[]> = {
  new: ["ready"],
  ready: ["running"],
  running: ["ready", "waiting", "terminated"],
  waiting: ["ready"],
  terminated: [],
};

function move(from: State, to: State): State {
  if (!transitions[from].includes(to)) {
    throw new Error("illegal transition: " + from + " -> " + to);
  }
  return to;
}

let state: State = "new";
for (const next of ["ready", "running", "waiting", "ready", "running", "terminated"] as State[]) {
  const before = state;
  state = move(state, next);
  console.log(before + " -> " + state);
}

try {
  move("waiting", "running");
} catch (error) {
  console.log((error as Error).message);
}
Output
new -> ready
ready -> running
running -> waiting
waiting -> ready
ready -> running
running -> terminated
illegal transition: waiting -> running

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1type State = "new" | "ready" | "running" | "waiting" | "terminated";
2
3const transitions: Record<State, State[]> = {
4 new: ["ready"],
5 ready: ["running"],
6 running: ["ready", "waiting", "terminated"],
7 waiting: ["ready"],
8 terminated: [],
9};
10
11function move(from: State, to: State): State {
12 if (!transitions[from].includes(to)) {
13 throw new Error("illegal transition: " + from + " -> " + to);
14 }
15 return to;
16}
17
18let state: State = "new";
19for (const next of ["ready", "running", "waiting", "ready", "running", "terminated"] as State[]) {
20 const before = state;
21 state = move(state, next);
22 console.log(before + " -> " + state);
23}
24
25try {
26 move("waiting", "running");
27} catch (error) {
28 console.log((error as Error).message);
29}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Only these five strings are valid states.—
—
Every state must appear exactly once, or the compiler reports an error.—
—
Each step is checked against the table before it is applied.—
—
The process waits for I/O once, then returns to ready and finishes.—
new -> ready
ready -> running
running -> waiting
waiting -> ready
ready -> running
running -> terminated
A waiting process must go through the ready queue first, so this throws.—
new -> ready
ready -> running
running -> waiting
waiting -> ready
ready -> running
running -> terminated
The error message names the illegal transition.—
new -> ready
ready -> running
running -> waiting
waiting -> ready
ready -> running
running -> terminated
illegal transition: waiting -> running

Behind the scenes

How TypeScript processes this code

TypeScript checks types and emits JavaScript. Type annotations are removed before execution.

Cost and common mistakes

O(1) per transition (a lookup in a small array).

  • The cast as State[] is needed because a plain array literal of strings would be inferred as string[].
  • A table-driven design is easier to check than a pile of if statements. Adding a state forces you to update the table.
  • Real kernels have more states (such as suspended and zombie) and per-process control blocks.

Try it yourself

Which transition does the scheduler perform to give the CPU to a ready process?

Reveal answer and reasoning

ready -> running, which is called dispatching the process.

Further reading