ParaScript
A TypeScript dialect with reactive signals, integer ranges, a pipeline operator, edge-triggered handlers,
chained .catch / .finally / await, and compile-time function purity.
Files end in .pts. All extensions desugar to standard JavaScript at parse time. The output imports
a small npm package,
parabun-browser-shims, that provides the runtime side of the language.
Syntax
Signals and effects
A signal declaration creates a reactive cell. Bare reads inside a tracked context (an
effect, a derived, a when block, or another signal's RHS) compile to
.get(). Bare writes compile to .set(). A signal whose initializer reads other signals
is auto-promoted to a derived value.
signal count = 0;
signal doubled = count * 2; // derived; recomputes when count changes
effect { console.log(doubled); } // runs once now, again on each change
count++; // count.set(count.get() + 1)
Edge-triggered handlers
A when block fires its body once on each false→true transition of the predicate.
when not fires on the true→false transition. The predicate is tracked the same way an effect
body is.
signal score = 0;
when score >= 100 { unlockAchievement("century"); }
when not online { showOfflineBanner(); }
Reactive bindings
A ~> B desugars to effect(() => { B = A; }), an assignment that stays in sync.
A -> fn desugars to effect(() => { fn(A); }), a call binding. Both are shorthand
for the common single-statement effect.
signal name = "world";
name ~> document.title; // title tracks name
name -> console.log; // logs on every change
Ranges and pipelines
a..b is an exclusive integer range; a..=b is inclusive. The pipeline operator
|> threads a value through a sequence of unary calls.
for (const i of 0..n) work(i);
const evens = 0..=20 |> filter(i => i % 2 === 0);
const out = pixels |> map(p => p * 1.2) |> clamp(0, 255);
Error and await operators
..! is .catch. ..& is .finally. ..= is
await-assign in declaration position; combined, they remove the boilerplate from a
try/catch chain.
const data ..= fetch(url).then(r => r.json())
..! err => defaults
..& () => spinner.hide();
Compilation
ParaScript files (.pts) are parsed by Bun's transpiler, which understands the syntax. The output is
standard JavaScript with a handful of imports from para:* module specifiers.
To resolve those specifiers in a non-Bun runtime, alias them to parabun-browser-shims in your
bundler.
// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
resolve: {
alias: [{ find: /^para:(.*)$/, replacement: "parabun-browser-shims/$1" }],
},
});
The same one-line alias works for esbuild, webpack, and rollup. See the install guide for the variants.
Bun is required for the build step today. A standalone npm-installable transpiler
(@parascript/transpile) is on the roadmap.
Examples
Three worked projects, one per host environment. Each is a complete project with file layout, source, and build commands.
- Frontend — todo list with reactive DOM updates. Vite + vanilla DOM.
- Backend — WebSocket server with per-connection signals. Node.
- Edge — HTTP handler with per-request signals. Cloudflare Workers.
Related projects
Parabun is a fork of Bun that ships ParaScript natively (no build or alias step) along with native modules for parallel CPU work, raw CUDA / Metal kernels, V4L2 camera capture, ALSA audio, GGUF LLM inference, and GPIO / I²C / SPI on Linux SBCs.