An educational pure functional programming library in TypeScript
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

TypeScript 79.7%
MDX 19.6%
CSS 0.6%
Astro 0.1%
103 1 15

Clone this repository

https://tangled.org/oleksify.me/purus-ts https://tangled.org/did:plc:prri3hmhhkgf2oe7upswu72m/purus-ts
git@tangled.org:oleksify.me/purus-ts git@tangled.org:did:plc:prri3hmhhkgf2oe7upswu72m/purus-ts

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

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:

Philosophy#

purus-ts is an educational library demonstrating pure functional programming patterns in TypeScript:

  1. Errors as values — Use Result and typed Eff errors instead of exceptions
  2. Effects as data — Effects are instruction trees interpreted by the runtime
  3. Composition over inheritance — Build complex behavior from simple functions via pipe and flow
  4. Types encode invariants — Use phantom types to make illegal states unrepresentable

Inspired by Effect-TS, fp-ts, and Scala's ZIO.

License#

MIT