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 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*