← 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 next = config.states[machine.state]?.[event];
      if (next !== undefined) {
        machine.state = next;
      }
    },
    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.361226
  type: 'test'
  ...
# Subtest: valid transition changes state
ok 2 - valid transition changes state
  ---
  duration_ms: 0.067319
  type: 'test'
  ...
# Subtest: invalid transition is ignored
ok 3 - invalid transition is ignored
  ---
  duration_ms: 0.039939
  type: 'test'
  ...
# Subtest: can reflects available transitions
ok 4 - can reflects available transitions
  ---
  duration_ms: 0.04199
  type: 'test'
  ...
# Subtest: can updates after a transition
ok 5 - can updates after a transition
  ---
  duration_ms: 0.046279
  type: 'test'
  ...
# Subtest: multi-step sequence
ok 6 - multi-step sequence
  ---
  duration_ms: 0.039249
  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.04342
  type: 'test'
  ...
1..7
# tests 7
# suites 0
# pass 7
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 80.577519
Reasoning (chain-of-thought)
The user wants a TypeScript implementation of a typed finite state machine.
The interface is:
```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>;
```

Wait, the `states` type in the config is `Record<S, Partial<Record<E, S>>>`. This means for each state `S`, it maps a subset of events `E` to a next state `S`.
The implementation needs to:
- Keep track of the current state.
- `send(event)` checks if `config.states[currentState][event]` exists. If so, update `state`. Otherwise, do nothing.
- `can(event)` checks if `config.states[currentState][event]` exists.

Let's implement it carefully.

```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(s: S) {
      currentState = s;
    },
    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 ? event in transitions : false;
    }
  };
}
```

Wait, `config.states[currentState]` is of type `Partial<Record<E, S>>`.
`event in transitions` works, but TypeScript might complain about `transitions` being possibly undefined, or `event in transitions` type checking.
Actually, `config.states` is `Record<S, Partial<Record<E, S>>>`, so `config.states[currentState]` is definitely defined because `currentState` is of type `S`, and `Record<S, ...>` guarantees all keys of type `S` are present. But `S` is a generic string, so TypeScript might not know that `currentState` is a key of `config.states` unless we assert or use `as`.
Wait, `Record<S, T>` means an object with keys of type `S`. If `S` is `"idle" | "running"`, then `Record<S, ...>` has those keys.
Accessing `config.states[currentState]` is fine.

Let's 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 current: S = config.initial;

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

Wait, `config.states[current]?.[event]` might be `S | undefined`. That's fine.
But `config.states[current]` is `Partial<Record<E, S>>`. So `config.states[current][event]` is `S | undefined`.
Using optional chaining `?.` is safe.

Let's check types strictly.
`config.states` is `Record<S, Partial<Record<E, S>>>`.
`current` is `S`.
`config.states[current]` is `Partial<Record<E, S>>`.
`config.states[current][event]` is `S | undefined`.
So `next !== undefined` works.

One minor thing: `state` is exposed as a mutable property. The interface says `state: S;`. It doesn't specify getter/setter, but a plain property or getter/setter both satisfy `state: S`. I'll use a getter/setter or just a plain object with a mutable property. Actually, returning an object with `state` as a property that can be read and written is fine. I'll just use a simple object with a getter/setter or a plain property. Since it's returned as an object, I can just do:
```ts
  const machine: Machine<S, E> = {
    state: config.initial,
    send(event: E): void {
      const next = config.states[machine.state]?.[event];
      if (next !== undefined) {
        machine.state = next;
      }
    },
    can(event: E): boolean {
      return config.states[machine.state]?.[event] !== undefined;
    }
  };
  return machine;
```
This is simpler and avoids getter/setter syntax if not needed. But `machine.state` is mutable, so assigning to it works.

Wait, `config.states[machine.state]` might cause TypeScript to complain if `machine.state` is `S` and `config.states` expects keys of type `S`. It should be fine.

Let's test locally in mind:
```ts
const m = createMachine({
  initial: "idle",
  states: {
    idle: { start: "running" },
    running: { pause: "paused", stop: "idle" },
    paused: { start: "running", stop: "idle" },
  },
});
```
`S` is inferred as `"idle" | "running" | "paused"`.
`E` is inferred as `"start" | "pause" | "stop"`.
`config.states` type matches.
`machine.state` is `S`.
`config.states[machine.state]` works.

One detail: `config.states` is `Record<S, Partial<Record<E, S>>>`.
In TypeScript, `Record<S, T>` means `{ [K in S]: T }`.
So `config.states[machine.state]` is perfectly typed.

Is there any issue with `config.states[machine.state]?.[event]`?
`config.states[machine.state]` is `Partial<Record<E, S>>`.
`[event]` on that is `S | undefined`.
So `next !== undefined` correctly checks if the transition exists.

What about `can(event)`? It returns `boolean`. `config.states[machine.state]?.[event] !== undefined` returns `boolean`. Correct.

Let's consider if `state` should be a plain property or getter/setter. The interface says `state: S;`. A plain property satisfies it. I'll stick with plain property for simplicity, or use getter/setter if needed. Actually, plain property is fine.

Wait, the example shows:
```ts
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;            // "i
…[7494 chars elided]…
te]?.[event];
      if (next !== undefined) {
        machine.state = next;
      }
    },
    can(event: E): boolean {
      return config.states[machine.state]?.[event] !== undefined;
    }
  };
  return machine;
}
```
This is correct. I'll output it. 
One minor point: `config.states[machine.state]` might be `undefined` if `machine.state` is not in `config.states` due to type widening, but the type system guarantees it. Using `?.` covers runtime safety anyway.
All good. 
Output matches request.
Done. 
Proceeds. 
[Output Generation] -> *Self-Correction/Refinement during generation prep*: 
I should make sure the code block is exactly `solution.ts` content.
The prompt asks to "Implement **`solution.ts`** exporting a factory...".
I will wrap it in a code block.
No extra text.
Done. 
Output matches. 
Proceeds. 
[Final Check]
- `state` is mutable property: yes.
- `send` transitions if defined, else ignored: yes.
- `can` returns true iff transition defined: yes.
- Fully typed, strict mode compatible: yes.
- No `any`: yes.
- Matches signature: yes.
All good. 
Output matches. 
Proceeds. 
[Output Generation] (matches the refined code)
```typescript
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 next = config.states[machine.state]?.[event];
      if (next !== undefined) {
        machine.state = next;
      }
    },
    can(event: E): boolean {
      return config.states[machine.state]?.[event] !== undefined;
    }
  };
  return machine;
}
```
Looks solid.
Done. 
(Self-Correction/Note during thought: `config.states[machine.state]` might cause TS to complain if `S` is not a literal type, but it's fine. I'll keep it.)
All good.
Output matches.
Proceeds. 
[Final Output Generation] -> *Proceeds*