~/skills/typescript
TypeScript
where most of the bigger projects here ended up.
StudyFilter, Rivet, AreUHuman, TinyPaste, Inkline and MX's app layer are all TypeScript. a lot of that code is state machines pretending to be something else.
used in StudyFilter · Rivet · AreUHuman · MX · TinyPaste · Inkline
typescript · runs the files it shows
type factory
3 of 4 handlers have type errors, so they cannot run yet.
pick a source, then a sink. keyboard works: tab to a port and press enter.
type checker idle. wire something.
tab to a port, enter to pickesc drops the selected source
how this works
the machine runs exactly the files on the left: factory.ts decides whether a wire is allowed and compile.ts holds the three puzzles. the drawing around them is not shown. the type check is a tiny structural one written for this page, not the real TypeScript compiler.
// the rules behind the type factory. every wire you draw is checked by check(), a tiny// structural type check: is what this source sends assignable to what that sink asks for?// the machine on the page imports this file and calls these same functions. export type Prim = "string" | "number" | "boolean";export type Shape = Readonly<Record<string, Prim>>;export type TypeDef = { readonly name: string; readonly shape: Shape }; export const types = { ConnectEvent: { name: "ConnectEvent", shape: { url: "string" } }, MessagePayload: { name: "MessagePayload", shape: { text: "string", from: "string" } }, ErrorPayload: { name: "ErrorPayload", shape: { message: "string", code: "number" } }, TimeoutEvent: { name: "TimeoutEvent", shape: { message: "string", code: "string" } }, // what the sinks ask for. a sink may want less than a source sends Failure: { name: "Failure", shape: { message: "string" } }, Retryable: { name: "Retryable", shape: { code: "number" } },} as const satisfies Record<string, TypeDef>; export type SourceId = "CONNECT" | "MESSAGE" | "ERROR" | "TIMEOUT";export type SinkId = "onConnect" | "onMessage" | "onError" | "retry";export type Payload = Readonly<Record<string, string | number | boolean>>; export type Source = { id: SourceId; type: TypeDef; sample: Payload };export type Sink = { id: SinkId; signature: string; accepts: TypeDef };export type Wire = { from: SourceId; to: SinkId }; export const sources: readonly Source[] = [ { id: "CONNECT", type: types.ConnectEvent, sample: { url: "wss://relay.example" } }, { id: "MESSAGE", type: types.MessagePayload, sample: { text: "hello", from: "ham" } }, { id: "ERROR", type: types.ErrorPayload, sample: { message: "socket closed", code: 1006 } }, { id: "TIMEOUT", type: types.TimeoutEvent, sample: { message: "no pong", code: "408" } },]; export const sinks: readonly Sink[] = [ { id: "onConnect", signature: "(e: ConnectEvent)", accepts: types.ConnectEvent }, { id: "onMessage", signature: "(m: MessagePayload)", accepts: types.MessagePayload }, { id: "onError", signature: "(e: Failure)", accepts: types.Failure }, { id: "retry", signature: "(s: Retryable)", accepts: types.Retryable },]; /** The machine works when these four wires exist. */export const required: readonly Wire[] = [ { from: "CONNECT", to: "onConnect" }, { from: "MESSAGE", to: "onMessage" }, { from: "ERROR", to: "onError" }, { from: "ERROR", to: "retry" },]; export type Verdict = { ok: boolean; reason: string; detail: string }; /** Is `from` assignable to `to`? Every property the target wants must exist, with the same type. */export function check(from: TypeDef, to: TypeDef): Verdict { for (const [key, want] of Object.entries(to.shape)) { const have = from.shape[key]; const reason = `${from.name} is not assignable to ${to.name}`; if (have === undefined) return { ok: false, reason, detail: `property '${key}' is missing in ${from.name}` }; if (have !== want) return { ok: false, reason, detail: `property '${key}' is ${have}, but ${to.name} expects ${want}` }; } const extra = Object.keys(from.shape).filter((key) => !(key in to.shape)); return { ok: true, reason: `${from.name} is assignable to ${to.name}`, detail: extra.length ? `${extra.join(", ")} ${extra.length > 1 ? "are" : "is"} extra, and extra is fine` : "same shape", };} const find = <T extends { id: string }>(list: readonly T[], id: string) => list.find((item) => item.id === id); export function canConnect(from: SourceId, to: SinkId): Verdict { const source = find(sources, from); const sink = find(sinks, to); if (!source || !sink) return { ok: false, reason: "no such port", detail: `${from} to ${to}` }; return check(source.type, sink.accepts);} export const sameWire = (a: Wire, b: Wire) => a.from === b.from && a.to === b.to; /** Try to add a wire. Only compatible ones are kept. */export function connect(wires: readonly Wire[], from: SourceId, to: SinkId): { wires: Wire[]; verdict: Verdict; added: boolean } { const verdict = canConnect(from, to); const wire = { from, to }; if (!verdict.ok) return { wires: [...wires], verdict, added: false }; if (wires.some((existing) => sameWire(existing, wire))) { return { wires: [...wires], verdict: { ...verdict, detail: "already wired" }, added: false }; } return { wires: [...wires, wire], verdict, added: true };} export const disconnect = (wires: readonly Wire[], wire: Wire) => wires.filter((existing) => !sameWire(existing, wire)); export const missing = (wires: readonly Wire[]) => required.filter((need) => !wires.some((wire) => sameWire(wire, need))); export function show(payload: Payload): string { const parts = Object.entries(payload).map(([key, value]) => `${key}: ${typeof value === "string" ? `"${value}"` : value}`); return `{ ${parts.join(", ")} }`;} export type Delivery = { wire: Wire; ok: boolean; line: string }; /** Send each source's sample down its wires. A sink that does not compile yet cannot run. */export function run(wires: readonly Wire[], broken: readonly SinkId[] = []): Delivery[] { return wires.map((wire) => { const source = find(sources, wire.from); if (!source) return { wire, ok: false, line: `${wire.from}: no such source` }; if (broken.includes(wire.to)) return { wire, ok: false, line: `${wire.to}(...) does not compile yet` }; return { wire, ok: true, line: `${wire.to}(${show(source.sample)})` }; });}