Blog Hoch
Aug 2026·1 tup leghDe'wI' chenmoHta'

TypeScript Daq state machine mach

#typescript#design-patterns#frontend

component isLoading, isError, hasData, isEmpty separate booleans rap ghItlh vaj bug ship: cha' booleans true wa' poH, UI spinner 'ej error message stacked. State machine type level Daq fix: independent flags (nonsense combine) pagh, wa' named state poH wa', transitions clearly defined.

boolean-soup Qagh

component state state machines replace:

interface FetchState {
  isLoading: boolean;
  isError: boolean;
  data: string[] | null;
  error: string | null;
}

loS fields, wa'maH jav combinations, loS neH valid (loading, success, error, idle). isLoading: true 'ej isError: true wa' poH Dach pagh - qaw poQ, Hoch Daq, reH.

states union model

state machine "DaH nuq qaS" wa' tagged union (flags bag pagh):

type FetchState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; message: string };

DaH data status "success" vaj neH tu'; message status "error" vaj neH. TypeScript control-flow narrowing enforce:

function render(state: FetchState): string {
  switch (state.status) {
    case "idle":
      return "Nothing yet.";
    case "loading":
      return "Loading…";
    case "success":
      return `Got ${state.data.length} items.`; // data is known to exist here
    case "error":
      return `Failed: ${state.message}`; // message is known to exist here
  }
}

"error" branch Daq state.data access vaj TypeScript compile time reject - 2am user blank screen report pagh.

transitions chel

state wa' snapshot. state machine second half allowed transitions: nuq states pIm states Daq jaH, nuq event. events states rap model:

type FetchEvent =
  | { type: "FETCH" }
  | { type: "RESOLVE"; data: string[] }
  | { type: "REJECT"; message: string }
  | { type: "RESET" };

function transition(state: FetchState, event: FetchEvent): FetchState {
  switch (state.status) {
    case "idle":
      if (event.type === "FETCH") return { status: "loading" };
      return state;

    case "loading":
      if (event.type === "RESOLVE") return { status: "success", data: event.data };
      if (event.type === "REJECT") return { status: "error", message: event.message };
      return state;

    case "success":
    case "error":
      if (event.type === "RESET") return { status: "idle" };
      return state;
  }
}

function branch Hoch explicit allowed. "idle" "success" direct accident jump laHbe' - code path pagh. future requirement transition poQ vaj deliberately wa' place Daq chel (component three booleans wrong order set side effect pagh).

component Daq wire

transition function pure - side effects pagh, unit test Dun. actual async work wrap thin layer:

class FetchController {
  private state: FetchState = { status: "idle" };
  private listeners: Array<(s: FetchState) => void> = [];

  private setState(next: FetchState) {
    this.state = next;
    this.listeners.forEach((listener) => listener(next));
  }

  subscribe(listener: (s: FetchState) => void) {
    this.listeners.push(listener);
    listener(this.state);
  }

  async load(url: string) {
    this.setState(transition(this.state, { type: "FETCH" }));
    try {
      const res = await fetch(url);
      const data: string[] = await res.json();
      this.setState(transition(this.state, { type: "RESOLVE", data }));
    } catch (err) {
      const message = err instanceof Error ? err.message : "Unknown error";
      this.setState(transition(this.state, { type: "REJECT", message }));
    }
  }
}

state change Hoch transition Daq - "valid combinations neH reachable" guarantee controller whole lifetime Daq (type level one render neH pagh).

transitions test (components pagh)

transition plain function, dependencies pagh: path Hoch test, mount pagh:

test("FETCH moves idle to loading", () => {
  const result = transition({ status: "idle" }, { type: "FETCH" });
  expect(result).toEqual({ status: "loading" });
});

test("REJECT during loading carries the error message", () => {
  const result = transition(
    { status: "loading" },
    { type: "REJECT", message: "network down" },
  );
  expect(result).toEqual({ status: "error", message: "network down" });
});

test("FETCH while already loading is a no-op", () => {
  const state: FetchState = { status: "loading" };
  const result = transition(state, { type: "FETCH" });
  expect(result).toBe(state);
});

last test qatlh: duplicate FETCH event (loading already) restart pagh, request double-fire pagh. document 'ej enforce.

overkill

two-state toggle (open / closed) machinery poQ pagh - wa' boolean Dun. explicit state machine: component wej+ mutually exclusive states, transitions bugs source (loading states classic), if (isLoading && !isError && data) guards ghItlh.

yaj

Booleans multiply - loS flags = wa'maH jav states, law' nonsense. tagged union valid states neH. wa' transition function "nuq next" decide. compiler rest enforce.

chu'Git Branching Strategies Qu' lIjqanReflows 'ej Repaints: page jank qatlh