fpure
v0.2.5
Published
Pure, curried, functional programming utilities for TypeScript.
Maintainers
Readme
fpure
Pure, curried, functional programming utilities for TypeScript.
Inspired by Ramda and Sanctuary
Install
npm install fpureUsage Example
import { compose, add, map, filter, gt } from 'fpure';
compose(
filter(gt(3)),
map(add(1))
)([1, 2, 3, 4]) // => [3, 5] (add 1 to each, then keep values > 3)
Why fpure?
Comparison
| Metric | fpure | Ramda | Sanctuary |
|--------|--------------------------------------------------|-------|-----------|
| Bundle size (gzip) | 7 KB (full lib) | ~14 KB (minified) | ~30 KB |
| Zero runtime dependencies | Yes | Yes | No (type system deps) |
| Runtime type checks | No (types at compile time only) | No | Yes (runtime validation) |
| TypeScript declarations | Built-in, ships with package | Requires @types/ramda | Ships with package |
| Deep import for tree-shaking (CJS) | Supported (require('fpure/list/map')) | Not supported (single 351 KB dist/ramda.js) | N/A |
| Deep import for tree-shaking (ESM) | Not yet (CJS-only... works via bundler interop) | Supported (import map from 'ramda/es/map') | Supported |
| Per-function internal dependencies | 1 (curry) | 2–8 (_curry2, _dispatchable, _xmap, etc.) | Varies |
| Data-last sub(3)(10) | 7 (10 − 3) | −7 (3 − 10) | 7 |
| Data-last gt(3)(5) | true (5 > 3) | false (3 > 5) | true |
| Function count | 147 | ~300 | ~100 |
Key differentiators
Data-last for ALL binary ops. Ramda's sub, div, mod, and gt/lt/gte/lte use config-first ordering that produces surprising results (sub(3)(10) = -7, gt(3)(5) = false). fpure is consistently data-last: sub(3)(10) = 7, gt(3)(5) = true.
Minimal per-function overhead. Each Ramda function imports 2–8 internal helpers (_curry2, _dispatchable, _xfilter, etc.). Every fpure function imports only curry.
CJS tree-shakeable. fpure ships every function as a standalone CommonJS file. You can deep-import require('fpure/list/map') and get exactly that function + curry (no barrel overhead, no bundled internals). Ramda's CJS entry is a single 351 KB dist/ramda.js bundle with no CJS per-function alternative.
Zero runtime type overhead. Sanctuary validates types at runtime. fpure uses TypeScript types only, meaning zero runtime cost, faster execution, smaller bundles.
Table of Contents
Curry
curry
Wraps a function so partial application returns a curried function until all args are satisfied.
const add = curry((a: number, b: number) => a + b)
add(1, 2) // => 3
add(1)(2) // => 3
const inc = add(1)
inc(5) // => 6Function Combinators
compose
Right-to-left function composition.
compose(x => x + 1, x => x * 2)(5) // => 11 (5*2 + 1)pipe
Left-to-right function composition.
pipe(x => x + 1, x => x * 2)(5) // => 12 ((5+1)*2)identity
identity(5) // => 5
identity({a:1}) // => {a:1}always
Creates a function that always returns the given value.
const f = always(42)
f() // => 42
f(1, 2, 3) // => 42T
Always returns true.
F
Always returns false.
flip
Swaps the first two arguments of a function.
const sub = curry((a, b) => a - b)
flip(sub, 3, 10) // => 7 (calls sub(10, 3))apply
Applies a function to an argument.
apply(x => x + 1, 5) // => 6tap
Runs a side-effect function on a value, returns the value.
tap(console.log, 42) // logs 42, returns 42juxt
Applies a list of functions to the same arguments, returns results.
juxt([x => x + 1, x => x * 2])(5) // => [6, 10]once
Wraps a function so it only executes once. Subsequent calls return the cached result.
memoize
Wraps a function so results are cached by argument identity (JSON-serialized).
converge
Accepts a converging function and a list of branches. Returns a function that applies branches to its args, then feeds results to the converger.
converge((a, b) => a + b, [x => x * 2, x => x * 3])(5) // => 25 (10 + 15)Math
All binary math ops use data-last: op(config, value) = value op config.
add
add(4, 5) // => 9
add(4)(5) // => 9sub
sub(3, 10) // => 7 (10 - 3)
sub(3)(10) // => 7mul
mul(4, 5) // => 20div
div(2, 10) // => 5 (10 / 2)
div(2)(10) // => 5mod
mod(3, 10) // => 1 (10 % 3)mathMod
True modulo (not remainder). Only works with positive integer modulus.
mathMod(5, 17) // => 2
mathMod(5, -17) // => 3
mathMod(5, 17.2) // => NaN (non-integer)
mathMod(-5, 17) // => NaN (non-positive modulus)inc
inc(5) // => 6dec
dec(5) // => 4negate
negate(5) // => -5sum
sum([1, 2, 3, 4]) // => 10
sum([]) // => 0product
product([1, 2, 3, 4]) // => 24
product([]) // => 1mean
mean([1, 2, 3, 4]) // => 2.5median
median([1, 2, 3, 4, 5]) // => 3
median([1, 2, 3, 4]) // => 2.5min
min(3, 5) // => 3max
max(3, 5) // => 5clamp
clamp(1, 10, 5) // => 5
clamp(1, 10, 0) // => 1
clamp(1, 10, 15) // => 10minBy
const byLength = x => x.length
minBy(byLength, 'short', 'longer') // => 'short'maxBy
maxBy(byLength, 'short', 'longer') // => 'longer'Relation
All binary comparisons use data-last: cmp(threshold, value) = value > threshold.
gt
Greater than.
gt(3, 5) // => true (5 > 3)
gt(5, 3) // => false
gt(3)(5) // => true "is arg > 3?"
const gt5 = gt(5)
gt5(10) // => true
gt5(3) // => falselt
Less than.
lt(5, 3) // => true (3 < 5)
lt(3, 5) // => falsegte
Greater than or equal.
gte(3, 5) // => true
gte(3, 3) // => true
gte(5, 3) // => falselte
Less than or equal.
lte(5, 3) // => true
lte(3, 3) // => true
lte(3, 5) // => falseidentical
Same-value-zero equality (like Object.is).
identical(1, 1) // => true
identical(1, '1') // => false
identical(NaN, NaN) // => true
identical(0, -0) // => false
identical([], []) // => falseequals
Deep equality. Handles arrays, nested objects, Dates, RegExps, Maps, Sets, circular references.
equals([1, [2, 3]], [1, [2, 3]]) // => true
equals({a: 1, b: {c: 3}}, {a: 1, b: {c: 3}}) // => true
equals(new Date(0), new Date(0)) // => trueList
Data-last: the list is always the last argument.
map
map(x => x * 2, [1, 2, 3]) // => [2, 4, 6]filter
filter(x => x > 1, [1, 2, 3]) // => [2, 3]reject
reject(x => x > 1, [1, 2, 3]) // => [1]reduce
reduce((a, b) => a + b, 0, [1, 2, 3]) // => 6reduceRight
reduceRight((a, b) => a + b, 0, [1, 2, 3]) // => 6find
find(x => x > 1, [1, 2, 3]) // => 2
find(x => x > 10, [1, 2, 3]) // => undefinedfindIndex
findIndex(x => x > 1, [1, 2, 3]) // => 1findLast
findLast(x => x < 3, [1, 2, 3, 2, 1]) // => 1 (last match)findLastIndex
findLastIndex(x => x < 3, [1, 2, 3, 2, 1]) // => 4head
head([1, 2, 3]) // => 1
head([]) // => undefinedtail
tail([1, 2, 3]) // => [2, 3]last
last([1, 2, 3]) // => 3init
init([1, 2, 3]) // => [1, 2]take
take(2, [1, 2, 3, 4]) // => [1, 2]drop
drop(2, [1, 2, 3, 4]) // => [3, 4]takeWhile
takeWhile(x => x < 3, [1, 2, 3, 4]) // => [1, 2]dropWhile
dropWhile(x => x < 3, [1, 2, 3, 4]) // => [3, 4]takeLast
takeLast(2, [1, 2, 3, 4]) // => [3, 4]dropLast
dropLast(2, [1, 2, 3, 4]) // => [1, 2]slice
slice(1, 3, [1, 2, 3, 4]) // => [2, 3]splitAt
splitAt(2, [1, 2, 3, 4]) // => [[1, 2], [3, 4]]splitEvery
splitEvery(2, [1, 2, 3, 4, 5]) // => [[1, 2], [3, 4], [5]]remove
Removes count elements starting at start.
remove(1, 2, [1, 2, 3, 4]) // => [1, 4]insert
Inserts an element at the given index.
insert(1, 99, [1, 2, 3]) // => [1, 99, 2, 3]insertAll
Inserts multiple elements at the given index.
insertAll(1, [99, 100], [1, 2, 3]) // => [1, 99, 100, 2, 3]update
Replaces the element at the given index.
update(1, 99, [1, 2, 3]) // => [1, 99, 3]adjust
Applies a function to the element at the given index.
adjust(x => x * 10, 1, [1, 2, 3]) // => [1, 20, 3]append
append(4, [1, 2, 3]) // => [1, 2, 3, 4]prepend
prepend(0, [1, 2, 3]) // => [0, 1, 2, 3]concat
concat([1, 2], [3, 4]) // => [1, 2, 3, 4]flatten
Recursively flattens nested arrays.
flatten([1, [2, [3]], 4]) // => [1, 2, 3, 4]chain
Maps a function over a list and flattens (flatMap).
chain(x => [x, x * 2], [1, 2, 3]) // => [1, 2, 2, 4, 3, 6]interpose
Inserts a separator between elements.
interpose(', ', [1, 2, 3]) // => [1, ', ', 2, ', ', 3]intercalate
Inserts a separator list between sublists and flattens.
intercalate([', '], [[1], [2], [3]]) // => [1, ', ', 2, ', ', 3]repeat
repeat(5, 3) // => [5, 5, 5]times
Calls a function n times with the index.
times(x => x * 2, 5) // => [0, 2, 4, 6, 8]zip
zip([1, 2, 3], ['a', 'b', 'c']) // => [[1, 'a'], [2, 'b'], [3, 'c']]
zip([1, 2, 3], ['a', 'b']) // => [[1, 'a'], [2, 'b']]zipObj
zipObj(['a', 'b', 'c'], [1, 2, 3]) // => {a: 1, b: 2, c: 3}zipWith
zipWith((a, b) => a + b, [1, 2, 3], [10, 20, 30]) // => [11, 22, 33]groupBy
Groups list elements by a key function.
groupBy(x => x > 2 ? 'big' : 'small', [1, 2, 3, 4])
// => { small: [1, 2], big: [3, 4] }groupWith
Groups consecutive equal elements.
groupWith((a, b) => a === b, [1, 1, 2, 2, 3])
// => [[1, 1], [2, 2], [3]]sort
Creates a sorted copy (mutates nothing).
sort((a, b) => a - b, [3, 1, 2]) // => [1, 2, 3]sortBy
Sorts by a transform function.
sortBy(x => x.length, ['aaa', 'b', 'cc']) // => ['b', 'cc', 'aaa']asc
Ascending numeric sort.
asc([3, 1, 2]) // => [1, 2, 3]desc
Descending numeric sort.
desc([1, 2, 3]) // => [3, 2, 1]all
all(x => x > 0, [1, 2, 3]) // => true
all(x => x > 1, [1, 2, 3]) // => falseany
any(x => x > 2, [1, 2, 3]) // => true
any(x => x > 10, [1, 2, 3]) // => falsenone
none(x => x > 10, [1, 2, 3]) // => true
none(x => x > 1, [1, 2, 3]) // => falseincludes
includes(2, [1, 2, 3]) // => true
includes(4, [1, 2, 3]) // => falsewithout
Removes specified values.
without([1, 2], [1, 2, 1, 3, 2, 4]) // => [3, 4]difference
Elements in the second list not in the first.
difference([1, 2, 3], [2, 3, 4]) // => [4]intersection
Elements in both lists.
intersection([1, 2, 3], [2, 3, 4]) // => [2, 3]union
Unique elements from both lists.
union([1, 2, 3], [2, 3, 4]) // => [1, 2, 3, 4]uniq
uniq([1, 2, 1, 3, 2]) // => [1, 2, 3]uniqBy
Deduplicate by a key function.
uniqBy(x => x % 2, [1, 2, 3, 4, 5]) // => [1, 2]uniqWith
Deduplicate with a custom comparator.
uniqWith((a, b) => a % 2 === b % 2, [1, 2, 3, 4, 5]) // => [1, 2]aperture
Returns overlapping subsequences of length n.
aperture(2, [1, 2, 3, 4]) // => [[1, 2], [2, 3], [3, 4]]
aperture(3, [1, 2, 3, 4]) // => [[1, 2, 3], [2, 3, 4]]partition
Splits into [pass, fail] arrays.
partition(x => x % 2 === 0, [1, 2, 3, 4, 5])
// => [[2, 4], [1, 3, 5]]span
Splits into [takeWhile, rest].
span(x => x < 3, [1, 2, 3, 4, 1]) // => [[1, 2], [3, 4, 1]]pluck
Extracts a key from each object in a list.
pluck('a', [{a: 1}, {a: 2}]) // => [1, 2]project
Selects specific keys from a list of objects.
project(['a', 'b'], [{a: 1, b: 2, c: 3}, {a: 4, b: 5, c: 6}])
// => [{a: 1, b: 2}, {a: 4, b: 5}]fromPairs
fromPairs([['a', 1], ['b', 2]]) // => {a: 1, b: 2}toPairs
toPairs({a: 1, b: 2}) // => [['a', 1], ['b', 2]]reverse
reverse([1, 2, 3]) // => [3, 2, 1]length
length([1, 2, 3]) // => 3countBy
Counts occurrences by a key function.
countBy(x => x > 2 ? 'big' : 'small', [1, 2, 3, 4])
// => { small: 2, big: 2 }forEach
Runs a side effect for each element, returns the original array.
forEach(x => console.log(x), [1, 2, 3])
// logs 1, 2, 3 and returns [1, 2, 3]indexBy
Indexes a list by a key function. Last duplicate wins.
indexBy(x => x.id, [{id: 'a', v: 1}, {id: 'b', v: 2}])
// => { a: {id: 'a', v: 1}, b: {id: 'b', v: 2} }Object
keys
keys({a: 1, b: 2}) // => ['a', 'b']values
values({a: 1, b: 2}) // => [1, 2]entries
entries({a: 1, b: 2}) // => [['a', 1], ['b', 2]]has
Own-property check.
has('a', {a: 1}) // => true
has('toString', {}) // => falsepath
Safely access nested paths.
path(['a', 'b'], {a: {b: 42}}) // => 42
path(['a', 'c'], {a: {b: 42}}) // => undefinedpathOr
pathOr(99, ['a', 'c'], {a: {b: 42}}) // => 99prop
prop('a', {a: 42}) // => 42propOr
propOr(99, 'b', {a: 42}) // => 99props
props(['a', 'b'], {a: 1, b: 2, c: 3}) // => [1, 2]assoc
assoc('b', 2, {a: 1}) // => {a: 1, b: 2}
assoc('a', 2, {a: 1}) // => {a: 2}assocPath
assocPath(['a', 'b'], 42, {}) // => {a: {b: 42}}dissoc
dissoc('a', {a: 1, b: 2, c: 3}) // => {b: 2, c: 3}dissocPath
dissocPath(['a', 'b'], {a: {b: 1, c: 2}, d: 3})
// => {a: {c: 2}, d: 3}merge
merge({a: 1}, {b: 2}) // => {a: 1, b: 2}mergeAll
mergeAll([{a: 1}, {b: 2}, {c: 3}]) // => {a: 1, b: 2, c: 3}mergeLeft
Same as merge (left-biased).
mergeRight
Right-biased merge (right wins).
mergeRight({a: 1}, {a: 2}) // => {a: 2}mergeDeepRight
Deep recursive merge.
mergeDeepRight({a: {b: 1}}, {a: {c: 2}})
// => {a: {b: 1, c: 2}}pick
pick(['a', 'c'], {a: 1, b: 2, c: 3}) // => {a: 1, c: 3}pickBy
pickBy(v => typeof v === 'number', {a: 1, b: 'x', c: 3})
// => {a: 1, c: 3}omit
omit(['a', 'c'], {a: 1, b: 2, c: 3}) // => {b: 2}omitBy
omitBy(v => typeof v === 'number', {a: 1, b: 'x', c: 3})
// => {b: 'x'}where
Tests if an object matches a predicate spec.
where({a: x => x > 0, b: x => x.length > 1}, {a: 5, b: 'hi'})
// => truewhereEq
Tests if an object matches a value spec.
whereEq({a: 1, b: 'hi'}, {a: 1, b: 'hi', c: 3})
// => trueevolve
Recursively transforms an object's values by a spec.
evolve({a: x => x * 2, b: x => x + 1}, {a: 5, b: 10, c: 99})
// => {a: 10, b: 11, c: 99}applySpec
Creates a function that applies args to a spec of functions.
const spec = applySpec({sum: (...xs) => xs.reduce((a,b) => a+b), max: Math.max})
spec(1, 2, 3) // => {sum: 6, max: 3}Lenses
Lenses provide focused access and immutability on nested data.
lensProp
const l = lensProp('a')
view(l, {a: 1, b: 2}) // => 1
set(l, 42, {a: 1, b: 2}) // => {a: 42, b: 2}
over(l, x => x + 1, {a: 1, b: 2}) // => {a: 2, b: 2}lensPath
const l = lensPath(['a', 'b', 'c'])
view(l, {a: {b: {c: 42}}}) // => 42
set(l, 99, {a: {b: {c: 42}}}) // => {a: {b: {c: 99}}}lensIndex
const l = lensIndex(1)
view(l, [10, 20, 30]) // => 20
set(l, 99, [10, 20, 30]) // => [10, 99, 30]
over(l, x => x + 5, [10, 20, 30]) // => [10, 25, 30]view
Extracts the focused value.
set
Sets the focused value (returns new object, original unchanged).
over
Applies a function to the focused value.
String
toUpper
toUpper('hello') // => 'HELLO'toLower
toLower('HELLO') // => 'hello'trim
trim(' hello ') // => 'hello'split
split(',', 'a,b,c') // => ['a', 'b', 'c']join
join(',', ['a', 'b', 'c']) // => 'a,b,c'replace
replace('world', 'there', 'hello world') // => 'hello there'
replace(/o/g, '0', 'hello') // => 'hell0'test
test(/hello/, 'hello world') // => truematch
match(/h(\w)/, 'hello') // => ['he', 'e'] (RegExpMatchArray)startsWith
startsWith('hello', 'hello world') // => trueendsWith
endsWith('world', 'hello world') // => truepadStart
padStart(5, '0', '42') // => '00042'padEnd
padEnd(5, '0', '42') // => '42000'Type
type
Returns the internal [[Class]] name.
type({}) // => 'Object'
type([]) // => 'Array'
type(null) // => 'Null'
type(undefined) // => 'Undefined'
type(() => {}) // => 'Function'
type(/[A-Z]/) // => 'RegExp'is
Checks if a value is an instance of a constructor.
is(Array, []) // => true
is(RegExp, /x/) // => trueisNil
isNil(null) // => true
isNil(undefined) // => true
isNil(0) // => falseisArray
isArray([]) // => true
isArray({}) // => falseisObject
Plain object check (not array, not null).
isObject({}) // => true
isObject([]) // => false
isObject(null) // => falseisString
isString('hello') // => true
isString(42) // => falseisNumber
Excludes NaN.
isNumber(42) // => true
isNumber(NaN) // => falseisBoolean
isBoolean(true) // => true
isBoolean(false) // => true
isBoolean(0) // => falseisFunction
isFunction(() => {}) // => trueisDate
isDate(new Date()) // => trueisRegExp
isRegExp(/test/) // => trueisError
isError(new Error()) // => trueisPromise
Detects thenables.
isPromise(Promise.resolve()) // => true
isPromise({then: () => {}}) // => trueisInteger
isInteger(42) // => true
isInteger(3.14) // => falseisFiniteNum
isFiniteNum(42) // => true
isFiniteNum(Infinity) // => false
isFiniteNum(NaN) // => falseisNaNVal
isNaNVal(NaN) // => true
isNaNVal(42) // => falseDesign
Parameter Ordering
All binary operations follow data-last, config-first: the thing you're likely to partially apply comes first, the data comes last.
| Function | Call | Meaning | Ramda (for comparison) |
|----------|------|---------|----------------------|
| sub(3)(10) | 10 - 3 = 7 | subtract 3 from 10 | R.subtract(3,10) = -7 |
| div(2)(10) | 10 / 2 = 5 | divide 10 by 2 | R.divide(2,10) = 0.2 |
| gt(3)(5) | 5 > 3 = true | is arg > 3? | R.gt(3,5) = false |
| mod(3)(10) | 10 % 3 = 1 | 10 mod 3 | R.mod(3,10) = 3 |
Immutability
Every function returns a new value. Inputs are never mutated.
Purity
No side effects in logic functions. tap and forEach are the only exceptions since they're explicitly for side effects.
No Inference
Unlike Ramda, fpure never inspects argument types to guess your intent. TypeScript handles type safety at compile time. Runtime is strict.
Development
Clone the repo and make changes to lib/. TypeScript is compiled to dist/ before running tests.
npm install # install dev dependencies (TypeScript, c8)
npm test # compiles lib/ → dist/, then runs tests