purus-ts#
Pure functional TypeScript with typed errors, fiber concurrency, and zero dependencies.
"purus" is Latin for "pure"
Features#
- Branded Types — Create distinct types from primitives (
UserId,Email) - Refinements — Encode validated properties in the type system (
Positive,NonEmpty,Sorted) - Result & Option — Handle errors and nullability as values, not exceptions
- Pattern Matching — Exhaustive, type-safe matching on discriminated unions
- Optics — Composable, law-abiding immutable data access (Lens, Prism, Optional, Traversal)
- Effect System — Composable effects with typed errors and dependency injection
- Fiber Runtime — Cooperative concurrency with cancellation, racing, and parallelism
- Zero Dependencies — Pure TypeScript library with zero runtime dependencies
Installation#
npm install purus-ts
# or
bun add purus-ts
# or
pnpm add purus-ts
Quick Start#
import { pipe, ok, err, mapResult, match, succeed, flatMap, runPromise } from 'purus-ts'
// Result: errors as values
const divide = (a: number, b: number) =>
b === 0 ? err('division by zero') : ok(a / b)
const result = pipe(
divide(10, 2),
mapResult(x => x * 2)
)
match(result)({
Ok: ({ value }) => console.log(`Result: ${value}`),
Err: ({ error }) => console.log(`Error: ${error}`)
})
// Effects: async operations with typed errors
const fetchUser = pipe(
succeed({ id: 1, name: 'Alice' }),
flatMap(user => succeed(`Hello, ${user.name}!`))
)
runPromise(fetchUser).then(console.log) // "Hello, Alice!"
Core Concepts#
Branded Types#
Create distinct types that are incompatible at compile time:
import { brand, type Branded } from 'purus-ts'
type UserId = Branded<string, 'UserId'>
type OrderId = Branded<string, 'OrderId'>
const userId: UserId = brand('user-123')
const orderId: OrderId = brand('order-456')
// userId = orderId // Compile error!
Refinements#
Encode validated properties in the type system:
import { positive, nonNegative, type Refined } from 'purus-ts'
const price = positive(99.99) // Refined<number, 'Positive'> | undefined
if (price) {
// TypeScript knows price > 0
}
Result & Option#
Handle errors and nullability without exceptions:
import { ok, err, some, none, mapResult, getOrElse, pipe } from 'purus-ts'
// Result<T, E> = Ok<T> | Err<E>
const parseNumber = (s: string) => {
const n = Number(s)
return isNaN(n) ? err('not a number') : ok(n)
}
// Option<T> = Some<T> | None
const findUser = (id: string) =>
id === 'admin' ? some({ name: 'Admin' }) : none
pipe(findUser('admin'), getOrElse({ name: 'Guest' }))
Pattern Matching#
Exhaustive matching on tagged unions:
import { match, when } from 'purus-ts'
type Shape =
| { _tag: 'Circle'; radius: number }
| { _tag: 'Rectangle'; width: number; height: number }
const area = (shape: Shape) =>
match(shape)({
Circle: ({ radius }) => Math.PI * radius ** 2,
Rectangle: ({ width, height }) => width * height,
})
// Predicate-based matching
const grade = (score: number) =>
when(score)(
[s => s >= 90, () => 'A'],
[s => s >= 80, () => 'B'],
)(() => 'F')
Effects & Fibers#
Composable async operations with typed errors:
import {
succeed, fail, fromPromise, flatMap, catchAll,
fork, join, race, all, timeout, retry,
access, provide, runPromise, pipe
} from 'purus-ts'
// Dependency injection
type Config = { apiUrl: string }
const fetchData = pipe(
access<Config, string>(env => env.apiUrl),
flatMap(url => fromPromise(() => fetch(url).then(r => r.json())))
)
// Provide dependencies at the edge
runPromise(pipe(fetchData, provide({ apiUrl: 'https://api.example.com' })))
// Concurrency
const fast = pipe(sleep(10), mapEff(() => 'fast'))
const slow = pipe(sleep(100), mapEff(() => 'slow'))
runPromise(race(fast, slow)) // 'fast' wins, slow is cancelled
// Parallel execution
runPromise(all([fetchA, fetchB, fetchC])) // Run in parallel, fail fast
API Overview#
Foundations#
| Category | Exports |
|---|---|
| Brands | Branded, brand |
| Refinements | Refined, refine, positive, nonNegative, normalized, integer |
| Result | Result, Ok, Err, ok, err, mapResult, chainResult, unwrapOr, tryCatch |
| Option | Option, Some, None, some, none, fromNullable, mapOption, getOrElse, flatMapOption |
| Pattern Matching | match, matchOr, when, matchLiteral, matchResult, matchOption |
| Arrays | Arr, arr, sort, sortNum, sortBy, map, filter, head, last, binarySearch |
| Units | Quantity, meters, seconds, velocity, addQ, scaleQ |
| Typestate | Entity, entity, transition |
| Composition | pipe, flow, id, constant, flip, tap |
| Guards | Guard, and, or, isString, isNumber, isBoolean |
Effect System#
| Category | Exports |
|---|---|
| Types | Eff, Fiber, FiberId, Exit, FiberStatus |
| Constructors | succeed, fail, sync, attempt, async, fromPromise |
| Transformations | mapEff, flatMap, foldEff, catchAll |
| Dependencies | access, accessEff, provide |
| Concurrency | fork, join, interruptFiber, race, all |
| Combinators | sleep, timeout, retry, repeatEff, tapEff |
| Runners | runFiber, runPromise, runPromiseExit, runPromiseWith |
Examples#
See the examples/ directory for side-by-side comparisons with vanilla TypeScript:
- http-client — Retry, timeout, and typed errors
- workflow-engine — Branded types and typestate patterns
- task-queue — Fiber concurrency and dependency injection
Philosophy#
purus-ts is an educational library demonstrating pure functional programming patterns in TypeScript:
- Errors as values — Use
Resultand typedEfferrors instead of exceptions - Effects as data — Effects are instruction trees interpreted by the runtime
- Composition over inheritance — Build complex behavior from simple functions via
pipeandflow - Types encode invariants — Use phantom types to make illegal states unrepresentable
Inspired by Effect-TS, fp-ts, and Scala's ZIO.