← run

ts-05-state-machine

0.000
0/1 tests· typing
Challenge · difficulty 5/5
# Typed finite state machine

Implement **`solution.ts`** exporting a factory that builds a typed finite state machine:

```ts
export interface Machine<S extends string, E extends string> {
  state: S;
  send(event: E): void;
  can(event: E): boolean;
}

export function createMachine<S extends string, E extends string>(config: {
  initial: S;
  states: Record<S, Partial<Record<E, S>>>;
}): Machine<S, E>;
```

`config.states` maps each state to a partial map from an event to the next state.

The returned machine:

- starts in `config.initial` (exposed as the mutable property `state`);
- `send(event)` transitions to the target state if the **current** state defines a
  transition for `event`; otherwise it is **ignored** (the state is unchanged, no throw);
- `can(event)` returns `true` iff the current state defines a transition for `event`.

Example:

```ts
const m = createMachine({
  initial: "idle",
  states: {
    idle: { start: "running" },
    running: { pause: "paused", stop: "idle" },
    paused: { start: "running", stop: "idle" },
  },
});

m.state;            // "idle"
m.can("start");     // true
m.can("pause");     // false
m.send("pause");    // ignored
m.state;            // "idle"
m.send("start");
m.state;            // "running"
m.send("stop");
m.state;            // "idle"
```

Keep it fully typed (must pass `tsc --noEmit` in strict mode). Do not use `any` in the
public API.
tests/solution.test.ts
import { test } from "node:test";
import { strict as assert } from "node:assert";
import { createMachine } from "./solution.ts";

type S = "idle" | "running" | "paused";
type E = "start" | "pause" | "stop";

function make() {
  return createMachine<S, E>({
    initial: "idle",
    states: {
      idle: { start: "running" },
      running: { pause: "paused", stop: "idle" },
      paused: { start: "running", stop: "idle" },
    },
  });
}

test("starts in the initial state", () => {
  assert.equal(make().state, "idle");
});

test("valid transition changes state", () => {
  const m = make();
  m.send("start");
  assert.equal(m.state, "running");
});

test("invalid transition is ignored", () => {
  const m = make();
  m.send("pause"); // not allowed from idle
  assert.equal(m.state, "idle");
});

test("can reflects available transitions", () => {
  const m = make();
  assert.equal(m.can("start"), true);
  assert.equal(m.can("pause"), false);
  assert.equal(m.can("stop"), false);
});

test("can updates after a transition", () => {
  const m = make();
  m.send("start");
  assert.equal(m.can("pause"), true);
  assert.equal(m.can("stop"), true);
  assert.equal(m.can("start"), false);
});

test("multi-step sequence", () => {
  const m = make();
  m.send("start"); // running
  m.send("pause"); // paused
  assert.equal(m.state, "paused");
  m.send("start"); // running
  m.send("stop"); // idle
  assert.equal(m.state, "idle");
});

test("self / terminal states with no outgoing events ignore everything", () => {
  const m = createMachine<"on" | "done", "finish" | "go">({
    initial: "on",
    states: {
      on: { finish: "done" },
      done: {},
    },
  });
  m.send("finish");
  assert.equal(m.state, "done");
  m.send("go");
  m.send("finish");
  assert.equal(m.state, "done");
  assert.equal(m.can("go"), false);
});
Proposed solution
**`config.initial`**: "idle"
**`config.states`**: Record< "idle", "paused", "stop" >
**`config.states.can`**: "idle"
**`config.states.can.send`**: "idle"
```s
**`config.states.can.send`**: Record< "idle", "paused", "stop", "running" >
**`config.states.can.send**: Record< "idle", "paused", "stop", "running" >
**`config.states.can.send**: Record< "idle", "paused", "stop", "running" >
**`config.states.can.send**: Record< "idle", "paused", "stop", "running" >
**`config.states.can.send**: Record< "idle", "paused", "stop", "running" >
**`config.states.can.send**: Record< "idle", "paused", "stop", "running" >
**`config.states.can.send**: Record< "idle", "paused", "stop", "running" >
**`config.states.can.send**: Record< "idle", "paused", "stop", "running

Errors (stderr)

no code extracted from response