TypeScript: a process lifecycle as a typed state machine
Allow only legal transitions between process states and reject the rest.
On this page
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
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);
}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
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);}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
| Step / line | What happens | State after step | Output 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.