aljabr
v0.3.10
Published
A TypeScript library for modeling data with tagged union types (algebraic sum types), consuming them with exhaustive pattern matching, and composing reactive computations with signals
Downloads
1,239
Maintainers
Readme
Al-jabr (الجبر) — the Arabic word that gave us "algebra." Bringing structure to chaos is, as it turns out, an ancient art.
aljabr is a TypeScript library built around one idea: that algebraic data types shouldn't live in isolation. Define your tagged unions once, then compose them — through pattern matching, schema validation, reactive state, and reactive UI — using the same model throughout.
It started as a pattern-matching utility. It grew into a small, coherent standard library: the union-centric toolkit for TypeScript, without a runtime, fibers, or a DI container. Zero dependencies.
What's in the box
aljabr ships independent entry points. Use what you need; ignore what you don't.
| Entry point | What it gives you |
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
| aljabr | Tagged unions, exhaustive match(), structural patterns, is.* wildcards, select() extraction |
| aljabr/prelude | Result, Option, Validation, Signal, Derived, Store, List, DerivedArray, Dispatcher, Scope, Resource, watch, Effect |
| aljabr/schema | Type-safe decode/encode pipeline for external data; errors surface as a Validation |
| aljabr/signals | SolidJS-style convenience layer over the reactive primitives |
| aljabr/ui | Reactive UI layer — JSX, function components, pluggable renderer host |
| aljabr/ui/dom | DOM rendering target (domHost) for browser apps |
| aljabr/ui/canvas | Retained-mode 2D canvas renderer (createCanvasRenderer, Viewport, canvasHost) for diagramming / dataviz |
See the API Reference below for the full per-module surface, and the Guides for narrative docs.
A taste
A small reactive shape editor — touches unions, exhaustive matching, reactive state, and the UI layer in one go:
/** @jsxImportSource aljabr/ui */
import { union, match, type Union } from "aljabr";
import { Store, Derived } from "aljabr/prelude";
import { createRenderer } from "aljabr/ui";
import { domHost } from "aljabr/ui/dom";
const Shape = union({
Circle: (id: number, radius: number) => ({ id, radius }),
Rect: (id: number, w: number, h: number) => ({ id, w, h }),
});
type Shape = Union<typeof Shape>;
const area = (s: Shape) => match(s, {
Circle: ({ radius }) => Math.PI * radius ** 2,
Rect: ({ w, h }) => w * h,
});
const shapes = Store.create<Shape[]>([Shape.Circle(1, 5), Shape.Rect(2, 3, 4)]);
const total = Derived.create(() => shapes.reduce((sum, s) => sum + area(s), 0));
const rows = shapes.map(
s => <li>{area(s).toFixed(2)}</li>,
{ key: s => s.id },
);
const { mount } = createRenderer(domHost);
mount(() =>
<div>
<ul>{rows}</ul>
<p>Total: {() => total.get()?.toFixed(2)}</p>
<button onClick={() => shapes.push(Shape.Circle(Date.now(), 10))}>
Add Circle
</button>
</div>,
document.body,
);Shape is the substrate. match checks exhaustively. Store.create<Shape[]>([...]) returns a List — a reactive root-level array with per-index subscriptions and pathless mutations (push, pop, splice, move, set). .map(fn, { key }) produces a keyed DerivedArray, so the renderer reconciles by id instead of position. Derived.create(...) recomputes the total only when the list changes. The () => total.get()?.toFixed(2) child is a reactive region: only that one text node updates when total changes.
Try the demo
A small todo app lives at public/ — unions, Store, List, the iterator chain, and the DOM renderer wired together end-to-end. It runs against the local source build, so it's also the fastest way to poke at the library while hacking on it.
git clone https://github.com/jasuperior/aljabr.git
cd aljabr
npm install
npm run devThen open the URL Vite prints (typically http://localhost:5173).
Motivation
TypeScript discriminated unions are powerful but verbose. You define the type, the discriminant field, the type guards — and then switch statements the compiler can only partially verify. aljabr eliminates the ceremony and tightens the guarantees, then extends the same union model out to error handling, reactive state, schema validation, and UI rendering. You're not buying a pattern-matching utility and then reaching for four other libraries — the composition stays in-model.
What aljabr is not
aljabr's surface area now overlaps with several other libraries. None of them are wrong; they're aimed at different things.
vs. Effect-ts. Effect is a full runtime: fibers, service layer, structured concurrency, mature ecosystem. aljabr has no runtime — it's algebraic data types and the things that compose through them. Reach for Effect when you want the framework; reach for aljabr when you don't.
vs. ts-pattern. ts-pattern is structural pattern matching over arbitrary objects. aljabr's dispatch is tag-first and nominal — better for unions you define yourself, with clean serialization and shared variant behavior. ts-pattern is the better fit for matching over third-party shapes.
vs. React / Solid. aljabr's UI layer has no virtual DOM and no diff cycle; it renders a static tree once and surgically updates only the regions whose signal dependencies change — closer to Solid than React. Unlike either, the renderer host is pluggable: DOM, canvas, SSR, or anything you implement against RendererHost<N, E> are equal peers. Components are plain functions; there are no hooks, no rules-of-hooks, and no registration.
vs. Preact Signals / standalone signal libraries. Signals are the smallest piece of aljabr's reactive system. The prelude also ships Store (per-path subscriptions over structured objects), List / DerivedArray (per-index reactive arrays with keyed reconciliation), Dispatcher (validated transactional state), Scope / Resource (structured cleanup), and watch / Effect (reactive async with retry policies, timeouts, cancellation). If you want signals only, a dedicated signals library is lighter; if you want the reactive substrate to extend to structured state and resource lifetimes, that's what aljabr is for.
vs. Awaitly. Workflow-first vs. ADT-first. Awaitly orients around typed async step composition; aljabr orients around tagged unions, with async and reactive as things that compose through them. Worth reading the author's post on algebraic thinking in TypeScript.
Installation
npm install aljabr
# pnpm add aljabr
# yarn add aljabrAPI Reference
Core
union()— define a sum type and get variant constructorsmatch()— exhaustive pattern matching engineTrait<R>— declare required payload properties on impl classespred()— wrap a predicate for use inwhen()patternsis— type wildcards and combinatorsselect()— mark a pattern field for extractionwhen()— construct a pattern match armgetTag()— read the variant name from an instance- Type utilities —
Union<T>,FactoryPayload<T>,Variant<Tag, Payload, Impl>
Schema (aljabr/schema)
Schema.*— schema builders (string, number, object, variant, transform, …)decode()/encode()/roundtrip()— decode/encode pipelineDecodeError— TypeMismatch, MissingField, InvalidLiteral, UnrecognizedVariant, Custom
Signals (aljabr/signals)
signal()/memo()/effect()/scope()/query()— convenience reactive API
UI (aljabr/ui)
- UI overview — choosing a renderer, shared core vs. per-renderer surfaces
- DOM renderer (
aljabr/ui/dom)view()/Fragment/ViewNode— element, component, and fragment factoriescreateRenderer()/mount()— renderer factory and mountingRendererHost<N, E>— contract for custom rendering targetsdomHost— production DOM implementation- JSX reference — tsconfig setup and JSX/
view()equivalence
- Canvas renderer (
aljabr/ui/canvas)createCanvasRenderer()/Viewport()/canvasHost— pre-wired renderer, pan/zoom factory, retained-mode hostCanvasNodeunion + intrinsic elements —rect,circle,ellipse,line,path,group,text- Inherited paint props + text layout —
<group>context, layout-driven labels - Events +
onHitTest— bubbling synthetic events, pixel-perfect override
Prelude (aljabr/prelude)
- Prelude overview — all modules at a glance
Result<T, E>Option<T>Validation<T, E>Signal<T, S>— reactive mutable container; custom state protocolsDerived<T>/AsyncDerived<T, E>— lazy computed reactive valuesStore<T>— structured reactive objects with per-path subscriptionsList<T>— reactive root-level array; pathless mutations, per-index reads, iterator methodsDerivedArray<T>— read-only per-index reactive view; key-based incremental diffing; chainablemap/filter/sortDispatcher<T, S, Cmd>— reactive container whose writes route through a typedapplyreturningValidationScope/Resource— structured resource lifetimes (constructed via.create(),Symbol.dispose/Symbol.asyncDispose)Effect<T, E>/watch— reactive async effects (watchreplaceswatchEffect)CommandError— extensible error union forDispatcher.applyfailuresFault<E>— classify async failuresSchedule/AsyncOptions— retry policies and timeoutsTree<T>— recursive binary tree- Persistence —
Signal.persisted,signal.persist(opts)returning aWatchHandle - Reactive context —
batch,untrack,createOwner
Guides
- Building UI with aljabr
- DOM renderer — static tree → reactive regions → components → lifecycle → reactive lists
- Canvas renderer — primitives,
Viewportpan/zoom, layout-driven labels, events
- Getting Started — first union through real-world patterns
- Union Patterns —
is.*,select(), destructuring, guards - Schema — decoding external data, error paths, object modes, variant mapping
- Resilient Async — retry, backoff, timeouts,
AbortSignal - Advanced Patterns
- Union Branching — Result chaining, Option as null discipline
- Signal Protocols — domain-specific signal state machines
- Reactive UI Patterns — Ref + Derived + AsyncDerived composition for complex data-layer state
- Resource Lifetime — Scope boundaries, bracket patterns
- Parser Construction — token/AST unions, recursive match
- Canvas Internals — paint pass, hit-test, implicit text wrapping
