Skip to content

Getting Started

Terminal window
bun add @stopcock/fp
import { flow, pipe } from '@stopcock/fp'
import * as A from '@stopcock/fp/array'
import * as O from '@stopcock/fp/option'
import * as R from '@stopcock/fp/result'

Pass a value through functions, left to right.

type User = { name: string; active: boolean; score: number }
const leaderboard = pipe(
users,
A.filter((u: User) => u.active && u.score > 0),
A.sortBy((a: User, b: User) => b.score - a.score),
A.take(10),
A.map((u: User) => u.name),
)

Every operator is curried and data-last: A.take(10) returns a function that takes the array, so it slots straight into pipe. There’s no data-first form (A.take(users, 10)) any more.

Same as pipe but gives you a reusable function instead of running straight away.

const activeNames = flow(
A.filter((u: User) => u.active),
A.map((u: User) => u.name),
)
activeNames(usersA)
activeNames(usersB)

pipe and flow are plain function application. Nothing fuses at runtime on its own:

const big = Array.from({ length: 1_000_000 }, (_, i) => i)
// three full passes over 1,000,000 items: filter, then map, then take(10)
pipe(
big,
A.filter((x: number) => x % 7 === 0),
A.map((x: number) => x * 2),
A.take(10),
)

For a pipeline you’ve actually measured as hot, add @stopcock/fp-compiler (a build-time Vite/Rollup/esbuild plugin). It recognizes this exact shape and rewrites it into one inlined loop that stops after 10 matches, so the call above only visits a few dozen items once it’s built. You don’t change how you write the pipeline — run stopcock check to see which call sites it recognized. See Fusion for how the compiler works.

pipe(
O.fromNullable(user.email),
O.map((e: string) => e.split('@')[1]),
O.getOrElse(() => 'unknown'),
)
pipe(
R.tryCatch(() => JSON.parse(input)),
R.map((obj: { value: number }) => obj.value),
R.getOrElse(() => null),
)

See Option & Result for the full API, or try the Cookbook for more patterns.