ts-05-state-machine
1.000
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.