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> {
let currentState: S = config.initial;
return {
get state(): S {
return currentState;
},
send(event: E): void {
const transitions = config.states[currentState];
if (transitions && event in transitions) {
currentState = transitions[event]!;
}
},
can(event: E): boolean {
const transitions = config.states[currentState];
return transitions && event in transitions;
},
};
}
```Test output (stdout)
TAP version 13 # Subtest: starts in the initial state ok 1 - starts in the initial state --- duration_ms: 0.375583 type: 'test' ... # Subtest: valid transition changes state ok 2 - valid transition changes state --- duration_ms: 0.06875 type: 'test' ... # Subtest: invalid transition is ignored ok 3 - invalid transition is ignored --- duration_ms: 0.04138 type: 'test' ... # Subtest: can reflects available transitions ok 4 - can reflects available transitions --- duration_ms: 0.04484 type: 'test' ... # Subtest: can updates after a transition ok 5 - can updates after a transition --- duration_ms: 0.045711 type: 'test' ... # Subtest: multi-step sequence ok 6 - multi-step sequence --- duration_ms: 0.540084 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.07175 type: 'test' ... 1..7 # tests 7 # suites 0 # pass 7 # fail 0 # cancelled 0 # skipped 0 # todo 0 # duration_ms 75.304166