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 { initial, states } = config;
let currentState: S = initial;
const can = (event: E): boolean => {
const stateTransitions = states[currentState];
return stateTransitions !== undefined && event in stateTransitions;
};
const send = (event: E): void => {
const stateTransitions = states[currentState];
if (stateTransitions !== undefined && event in stateTransitions) {
currentState = stateTransitions[event];
}
};
return {
get state() {
return currentState;
},
send,
can,
};
}
```Test output (stdout)
TAP version 13 # Subtest: starts in the initial state ok 1 - starts in the initial state --- duration_ms: 0.809251 type: 'test' ... # Subtest: valid transition changes state ok 2 - valid transition changes state --- duration_ms: 0.07562 type: 'test' ... # Subtest: invalid transition is ignored ok 3 - invalid transition is ignored --- duration_ms: 0.042701 type: 'test' ... # Subtest: can reflects available transitions ok 4 - can reflects available transitions --- duration_ms: 0.04444 type: 'test' ... # Subtest: can updates after a transition ok 5 - can updates after a transition --- duration_ms: 0.04749 type: 'test' ... # Subtest: multi-step sequence ok 6 - multi-step sequence --- duration_ms: 0.03914 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.04348 type: 'test' ... 1..7 # tests 7 # suites 0 # pass 7 # fail 0 # cancelled 0 # skipped 0 # todo 0 # duration_ms 83.906112
Reasoning (chain-of-thought)
The user wants me to implement a typed finite state machine in TypeScript. Let me break down the requirements:
1. Create a `Machine` interface with:
- `state: S` - the current state
- `send(event: E): void` - transitions if possible, otherwise ignores
- `can(event: E): boolean` - checks if transition is possible
2. Create a `createMachine` function that:
- Takes `initial: S` and `states: Record<S, Partial<Record<E, S>>>`
- Returns a `Machine<S, E>`
3. Key behaviors:
- `send(event)` only transitions if the current state has a transition for that event
- `can(event)` returns true if current state has a transition for that event
- Must be fully typed with no `any` in public API
- Must pass `tsc --noEmit` in strict mode
Let me think about the typing:
For `states`, it's `Record<S, Partial<Record<E, S>>>`. This means:
- For each state S, we have a partial record of events E that map to next states S
- `Partial` means not all events need to be defined for each state
For `can(event: E)`, I need to check if the current state has a transition for that event.
For `send(event: E)`, I need to check if the current state has a transition for that event, and if so, update the state.
The tricky part is making this fully typed. I need to ensure that:
1. `can(event)` only accepts events that are actually defined in the states
2. `send(event)` only accepts events that are actually defined in the states
3. The state transitions are typed correctly
Let me think about how to type this properly:
```ts
export interface Machine<S extends string, E extends string> {
state: S;
send(event: E): void;
can(event: E): boolean;
}
```
The issue is that `E` is a generic string, but we want to restrict it to only events that are actually defined in the states. However, the interface signature uses `E extends string`, so I need to work within that constraint.
I'll need to use TypeScript's type system to ensure type safety while maintaining the generic interface. The key is to leverage the `states` configuration to create a type-safe implementation that prevents invalid state transitions.
The implementation will require careful type checking to ensure only valid events can be sent and checked against the current state. I'll focus on creating a robust type system that prevents runtime errors through compile-time type checking.
The core challenge is creating a type-safe mechanism that allows dynamic state transitions while maintaining strict type constraints. This means using TypeScript's advanced type features like mapped types and conditional types to enforce the state machine's rules.
I'll need to carefully design the type system to ensure that only valid events can trigger state changes, and that the machine's behavior is predictable and type-safe.