Pure, curried, functional programming utilities for TypeScript.
Inspired by Ramda and Sanctuary
npm install fpureimport { 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)| 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/dist/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 |
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/dist/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.
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) // => 6Right-to-left function composition.
compose(x => x + 1, x => x * 2)(5) // => 11 (5*2 + 1)Left-to-right function composition.
pipe(x => x + 1, x => x * 2)(5) // => 12 ((5+1)*2)identity(5) // => 5
identity({a:1}) // => {a:1}Creates a function that always returns the given value.
const f = always(42)
f() // => 42
f(1, 2, 3) // => 42Always returns true.
Always returns false.
Swaps the first two arguments of a function.
const sub = curry((a, b) => a - b)
flip(sub, 3, 10) // => 7 (calls sub(10, 3))Applies a function to an argument.
apply(x => x + 1, 5) // => 6Runs a side-effect function on a value, returns the value.
tap(console.log, 42) // logs 42, returns 42Applies a list of functions to the same arguments, returns results.
juxt([x => x + 1, x => x * 2])(5) // => [6, 10]Wraps a function so it only executes once. Subsequent calls return the cached result.
Wraps a function so results are cached by argument identity (JSON-serialized).
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)All binary math ops use data-last: op(config, value) = value op config.
add(4, 5) // => 9
add(4)(5) // => 9sub(3, 10) // => 7 (10 - 3)
sub(3)(10) // => 7mul(4, 5) // => 20div(2, 10) // => 5 (10 / 2)
div(2)(10) // => 5mod(3, 10) // => 1 (10 % 3)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(5) // => 6dec(5) // => 4negate(5) // => -5sum([1, 2, 3, 4]) // => 10
sum([]) // => 0product([1, 2, 3, 4]) // => 24
product([]) // => 1mean([1, 2, 3, 4]) // => 2.5median([1, 2, 3, 4, 5]) // => 3
median([1, 2, 3, 4]) // => 2.5min(3, 5) // => 3max(3, 5) // => 5clamp(1, 10, 5) // => 5
clamp(1, 10, 0) // => 1
clamp(1, 10, 15) // => 10const byLength = x => x.length
minBy(byLength, 'short', 'longer') // => 'short'maxBy(byLength, 'short', 'longer') // => 'longer'All binary comparisons use data-last: cmp(threshold, value) = value > threshold.
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) // => falseLess than.
lt(5, 3) // => true (3 < 5)
lt(3, 5) // => falseGreater than or equal.
gte(3, 5) // => true
gte(3, 3) // => true
gte(5, 3) // => falseLess than or equal.
lte(5, 3) // => true
lte(3, 3) // => true
lte(3, 5) // => falseSame-value-zero equality (like Object.is).
identical(1, 1) // => true
identical(1, '1') // => false
identical(NaN, NaN) // => true
identical(0, -0) // => false
identical([], []) // => falseDeep 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)) // => trueData-last: the list is always the last argument.
map(x => x * 2, [1, 2, 3]) // => [2, 4, 6]filter(x => x > 1, [1, 2, 3]) // => [2, 3]reject(x => x > 1, [1, 2, 3]) // => [1]reduce((a, b) => a + b, 0, [1, 2, 3]) // => 6reduceRight((a, b) => a + b, 0, [1, 2, 3]) // => 6find(x => x > 1, [1, 2, 3]) // => 2
find(x => x > 10, [1, 2, 3]) // => undefinedfindIndex(x => x > 1, [1, 2, 3]) // => 1findLast(x => x < 3, [1, 2, 3, 2, 1]) // => 1 (last match)findLastIndex(x => x < 3, [1, 2, 3, 2, 1]) // => 4head([1, 2, 3]) // => 1
head([]) // => undefinedtail([1, 2, 3]) // => [2, 3]last([1, 2, 3]) // => 3init([1, 2, 3]) // => [1, 2]take(2, [1, 2, 3, 4]) // => [1, 2]drop(2, [1, 2, 3, 4]) // => [3, 4]takeWhile(x => x < 3, [1, 2, 3, 4]) // => [1, 2]dropWhile(x => x < 3, [1, 2, 3, 4]) // => [3, 4]takeLast(2, [1, 2, 3, 4]) // => [3, 4]dropLast(2, [1, 2, 3, 4]) // => [1, 2]slice(1, 3, [1, 2, 3, 4]) // => [2, 3]splitAt(2, [1, 2, 3, 4]) // => [[1, 2], [3, 4]]splitEvery(2, [1, 2, 3, 4, 5]) // => [[1, 2], [3, 4], [5]]Removes count elements starting at start.
remove(1, 2, [1, 2, 3, 4]) // => [1, 4]Inserts an element at the given index.
insert(1, 99, [1, 2, 3]) // => [1, 99, 2, 3]Inserts multiple elements at the given index.
insertAll(1, [99, 100], [1, 2, 3]) // => [1, 99, 100, 2, 3]Replaces the element at the given index.
update(1, 99, [1, 2, 3]) // => [1, 99, 3]Applies a function to the element at the given index.
adjust(x => x * 10, 1, [1, 2, 3]) // => [1, 20, 3]append(4, [1, 2, 3]) // => [1, 2, 3, 4]prepend(0, [1, 2, 3]) // => [0, 1, 2, 3]concat([1, 2], [3, 4]) // => [1, 2, 3, 4]Recursively flattens nested arrays.
flatten([1, [2, [3]], 4]) // => [1, 2, 3, 4]Maps a function over a list and flattens (flatMap).
chain(x => [x, x * 2], [1, 2, 3]) // => [1, 2, 2, 4, 3, 6]Inserts a separator between elements.
interpose(', ', [1, 2, 3]) // => [1, ', ', 2, ', ', 3]Inserts a separator list between sublists and flattens.
intercalate([', '], [[1], [2], [3]]) // => [1, ', ', 2, ', ', 3]repeat(5, 3) // => [5, 5, 5]Calls a function n times with the index.
times(x => x * 2, 5) // => [0, 2, 4, 6, 8]zip([1, 2, 3], ['a', 'b', 'c']) // => [[1, 'a'], [2, 'b'], [3, 'c']]
zip([1, 2, 3], ['a', 'b']) // => [[1, 'a'], [2, 'b']]zipObj(['a', 'b', 'c'], [1, 2, 3]) // => {a: 1, b: 2, c: 3}zipWith((a, b) => a + b, [1, 2, 3], [10, 20, 30]) // => [11, 22, 33]Groups list elements by a key function.
groupBy(x => x > 2 ? 'big' : 'small', [1, 2, 3, 4])
// => { small: [1, 2], big: [3, 4] }Groups consecutive equal elements.
groupWith((a, b) => a === b, [1, 1, 2, 2, 3])
// => [[1, 1], [2, 2], [3]]Creates a sorted copy (mutates nothing).
sort((a, b) => a - b, [3, 1, 2]) // => [1, 2, 3]Sorts by a transform function.
sortBy(x => x.length, ['aaa', 'b', 'cc']) // => ['b', 'cc', 'aaa']Ascending numeric sort.
asc([3, 1, 2]) // => [1, 2, 3]Descending numeric sort.
desc([1, 2, 3]) // => [3, 2, 1]all(x => x > 0, [1, 2, 3]) // => true
all(x => x > 1, [1, 2, 3]) // => falseany(x => x > 2, [1, 2, 3]) // => true
any(x => x > 10, [1, 2, 3]) // => falsenone(x => x > 10, [1, 2, 3]) // => true
none(x => x > 1, [1, 2, 3]) // => falseincludes(2, [1, 2, 3]) // => true
includes(4, [1, 2, 3]) // => falseRemoves specified values.
without([1, 2], [1, 2, 1, 3, 2, 4]) // => [3, 4]Elements in the second list not in the first.
difference([1, 2, 3], [2, 3, 4]) // => [4]Elements in both lists.
intersection([1, 2, 3], [2, 3, 4]) // => [2, 3]Unique elements from both lists.
union([1, 2, 3], [2, 3, 4]) // => [1, 2, 3, 4]uniq([1, 2, 1, 3, 2]) // => [1, 2, 3]Deduplicate by a key function.
uniqBy(x => x % 2, [1, 2, 3, 4, 5]) // => [1, 2]Deduplicate with a custom comparator.
uniqWith((a, b) => a % 2 === b % 2, [1, 2, 3, 4, 5]) // => [1, 2]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]]Splits into [pass, fail] arrays.
partition(x => x % 2 === 0, [1, 2, 3, 4, 5])
// => [[2, 4], [1, 3, 5]]Splits into [takeWhile, rest].
span(x => x < 3, [1, 2, 3, 4, 1]) // => [[1, 2], [3, 4, 1]]Extracts a key from each object in a list.
pluck('a', [{a: 1}, {a: 2}]) // => [1, 2]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([['a', 1], ['b', 2]]) // => {a: 1, b: 2}toPairs({a: 1, b: 2}) // => [['a', 1], ['b', 2]]reverse([1, 2, 3]) // => [3, 2, 1]length([1, 2, 3]) // => 3Counts occurrences by a key function.
countBy(x => x > 2 ? 'big' : 'small', [1, 2, 3, 4])
// => { small: 2, big: 2 }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]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} }keys({a: 1, b: 2}) // => ['a', 'b']values({a: 1, b: 2}) // => [1, 2]entries({a: 1, b: 2}) // => [['a', 1], ['b', 2]]Own-property check.
has('a', {a: 1}) // => true
has('toString', {}) // => falseSafely access nested paths.
path(['a', 'b'], {a: {b: 42}}) // => 42
path(['a', 'c'], {a: {b: 42}}) // => undefinedpathOr(99, ['a', 'c'], {a: {b: 42}}) // => 99prop('a', {a: 42}) // => 42propOr(99, 'b', {a: 42}) // => 99props(['a', 'b'], {a: 1, b: 2, c: 3}) // => [1, 2]assoc('b', 2, {a: 1}) // => {a: 1, b: 2}
assoc('a', 2, {a: 1}) // => {a: 2}assocPath(['a', 'b'], 42, {}) // => {a: {b: 42}}dissoc('a', {a: 1, b: 2, c: 3}) // => {b: 2, c: 3}dissocPath(['a', 'b'], {a: {b: 1, c: 2}, d: 3})
// => {a: {c: 2}, d: 3}merge({a: 1}, {b: 2}) // => {a: 1, b: 2}mergeAll([{a: 1}, {b: 2}, {c: 3}]) // => {a: 1, b: 2, c: 3}Same as merge (left-biased).
Right-biased merge (right wins).
mergeRight({a: 1}, {a: 2}) // => {a: 2}Deep recursive merge.
mergeDeepRight({a: {b: 1}}, {a: {c: 2}})
// => {a: {b: 1, c: 2}}pick(['a', 'c'], {a: 1, b: 2, c: 3}) // => {a: 1, c: 3}pickBy(v => typeof v === 'number', {a: 1, b: 'x', c: 3})
// => {a: 1, c: 3}omit(['a', 'c'], {a: 1, b: 2, c: 3}) // => {b: 2}omitBy(v => typeof v === 'number', {a: 1, b: 'x', c: 3})
// => {b: 'x'}Tests if an object matches a predicate spec.
where({a: x => x > 0, b: x => x.length > 1}, {a: 5, b: 'hi'})
// => trueTests if an object matches a value spec.
whereEq({a: 1, b: 'hi'}, {a: 1, b: 'hi', c: 3})
// => trueRecursively 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}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 provide focused access and immutability on nested data.
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}const l = lensPath(['a', 'b', 'c'])
view(l, {a: {b: {c: 42}}}) // => 42
set(l, 99, {a: {b: {c: 42}}}) // => {a: {b: {c: 99}}}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]Extracts the focused value.
Sets the focused value (returns new object, original unchanged).
Applies a function to the focused value.
toUpper('hello') // => 'HELLO'toLower('HELLO') // => 'hello'trim(' hello ') // => 'hello'split(',', 'a,b,c') // => ['a', 'b', 'c']join(',', ['a', 'b', 'c']) // => 'a,b,c'replace('world', 'there', 'hello world') // => 'hello there'
replace(/o/g, '0', 'hello') // => 'hell0'test(/hello/, 'hello world') // => truematch(/h(\w)/, 'hello') // => ['he', 'e'] (RegExpMatchArray)startsWith('hello', 'hello world') // => trueendsWith('world', 'hello world') // => truepadStart(5, '0', '42') // => '00042'padEnd(5, '0', '42') // => '42000'Returns the internal [[Class]] name.
type({}) // => 'Object'
type([]) // => 'Array'
type(null) // => 'Null'
type(undefined) // => 'Undefined'
type(() => {}) // => 'Function'
type(/[A-Z]/) // => 'RegExp'Checks if a value is an instance of a constructor.
is(Array, []) // => true
is(RegExp, /x/) // => trueisNil(null) // => true
isNil(undefined) // => true
isNil(0) // => falseisArray([]) // => true
isArray({}) // => falsePlain object check (not array, not null).
isObject({}) // => true
isObject([]) // => false
isObject(null) // => falseisString('hello') // => true
isString(42) // => falseExcludes NaN.
isNumber(42) // => true
isNumber(NaN) // => falseisBoolean(true) // => true
isBoolean(false) // => true
isBoolean(0) // => falseisFunction(() => {}) // => trueisDate(new Date()) // => trueisRegExp(/test/) // => trueisError(new Error()) // => trueDetects thenables.
isPromise(Promise.resolve()) // => true
isPromise({then: () => {}}) // => trueisInteger(42) // => true
isInteger(3.14) // => falseisFiniteNum(42) // => true
isFiniteNum(Infinity) // => false
isFiniteNum(NaN) // => falseisNaNVal(NaN) // => true
isNaNVal(42) // => falseAll 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 |
Every function returns a new value. Inputs are never mutated.
No side effects in logic functions. tap and forEach are the only exceptions since they're explicitly for side effects.
Unlike Ramda, fpure never inspects argument types to guess your intent. TypeScript handles type safety at compile time. Runtime is strict.
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