โ† run

ts-05-state-machine

1.000
7/7 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);
});
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```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> {
  const machine: Machine<S, E> = {
    state: config.initial,
    send(event: E): void {
      const nextState = config.states[machine.state]?.[event];
      if (nextState !== undefined) {
        machine.state = nextState;
      }
    },
    can(event: E): boolean {
      return config.states[machine.state]?.[event] !== undefined;
    },
  };
  return machine;
}
```

Test output (stdout)

TAP version 13
# Subtest: starts in the initial state
ok 1 - starts in the initial state
  ---
  duration_ms: 0.353111
  type: 'test'
  ...
# Subtest: valid transition changes state
ok 2 - valid transition changes state
  ---
  duration_ms: 0.06591
  type: 'test'
  ...
# Subtest: invalid transition is ignored
ok 3 - invalid transition is ignored
  ---
  duration_ms: 0.03982
  type: 'test'
  ...
# Subtest: can reflects available transitions
ok 4 - can reflects available transitions
  ---
  duration_ms: 0.048351
  type: 'test'
  ...
# Subtest: can updates after a transition
ok 5 - can updates after a transition
  ---
  duration_ms: 0.0452
  type: 'test'
  ...
# Subtest: multi-step sequence
ok 6 - multi-step sequence
  ---
  duration_ms: 0.03926
  type: 'test'
  ...
# Subtest: self / terminal states with no outgoing events ignore everything
ok 7 - self / terminal states with no outgoing events ignore everything
  ---
  duration_ms: 0.04513
  type: 'test'
  ...
1..7
# tests 7
# suites 0
# pass 7
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 79.118887
Reasoning (chain-of-thought)
The task is to implement a typed finite state machine factory in TypeScript. Let me analyze the requirements:

1. `Machine<S, E>` interface has:
   - `state: S` - mutable property for current state
   - `send(event: E): void` - transitions if possible, otherwise ignored
   - `can(event: E): boolean` - checks if transition is possible

2. `createMachine` takes a config with:
   - `initial: S` - starting state
   - `states: Record<S, Partial<Record<E, S>>>` - transition map

3. Behavior:
   - `send(event)` transitions only if current state has a transition for that event
   - If no transition, state is unchanged, no throw
   - `can(event)` returns true if current state has transition for event

Let me implement this:

```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> {
  let currentState: S = config.initial;

  return {
    get state(): S {
      return currentState;
    },
    set state(value: S) {
      currentState = value;
    },
    send(event: E): void {
      const transitions = config.states[currentState];
      if (transitions && event in transitions) {
        currentState = transitions[event] as S;
      }
    },
    can(event: E): boolean {
      const transitions = config.states[currentState];
      return transitions != null && event in transitions;
    },
  };
}
```

Wait, let me think about the typing more carefully. The `states` is `Record<S, Partial<Record<E, S>>>`. When we access `config.states[currentState]`, TypeScript should know it's `Partial<Record<E, S>>`. Then `event in transitions` checks if the event is a key.

Actually, there's a subtle issue. `Partial<Record<E, S>>` means each event maps to `S | undefined`. So `transitions[event]` would be `S | undefined`. We need to check if it's defined.

Let me refine:

```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> {
  let currentState: S = config.initial;

  return {
    get state(): S {
      return currentState;
    },
    set state(value: S) {
      currentState = value;
    },
    send(event: E): void {
      const transitions = config.states[currentState];
      const nextState = transitions?.[event];
      if (nextState !== undefined) {
        currentState = nextState;
      }
    },
    can(event: E): boolean {
      const transitions = config.states[currentState];
      return transitions?.[event] !== undefined;
    },
  };
}
```

This should work. The `state` property is mutable via getter/setter. `send` checks if there's a transition and updates if so. `can` checks if there's a transition.

Let me verify with the example:
- Initial state is "idle"
- `m.state` โ†’ "idle" โœ“
- `m.can("start")` โ†’ `config.states["idle"]["start"]` is "running", so true โœ“
- `m.can("pause")` โ†’ `config.states["idle"]["pause"]` is undefined, so false โœ“
- `m.send("pause")` โ†’ no transition, ignored โœ“
- `m.state` โ†’ "idle" โœ“
- `m.send("start")` โ†’ transitions to "running" โœ“
- `m.state` โ†’ "running" โœ“
- `m.send("stop")` โ†’ transitions to "idle" โœ“
- `m.state` โ†’ "idle" โœ“

This looks correct. Let me also make sure it passes strict mode TypeScript. The types look correct - no `any` used, generics are properly constrained.

Actually, I should double-check: the `state` property needs to be mutable. Using getter/setter should work. Alternatively, I could use a plain object with a mutable property, but then I'd need to be careful about the return type.

Actually, a simpler approach would be:

```ts
export function createMachine<S extends string, E extends string>(config: {
  initial: S;
  states: Record<S, Partial<Record<E, S>>>;
}): Machine<S, E> {
  const machine: Machine<S, E> = {
    state: config.initial,
    send(event: E): void {
      const nextState = config.states[machine.state]?.[event];
      if (nextState !== undefined) {
        machine.state = nextState;
      }
    },
    can(event: E): boolean {
      return config.states[machine.state]?.[event] !== undefined;
    },
  };
  return machine;
}
```

This is cleaner and the `state` property is directly mutable. This should work fine with TypeScript strict mode.

Let me verify the types:
- `machine.state` is of type `S` โœ“
- `config.states[machine.state]` is of type `Partial<Record<E, S>>` โœ“
- `config.states[machine.state]?.[event]` is of type `S | undefined` โœ“
- The assignment `machine.state = nextState` is valid since `nextState` is `S` (after the undefined check) โœ“

This looks good.