Functional operators library optimized for JSON DSL - Ramda-inspired, TypeScript-first
npm install @statedelta-libs/operators> Functional operators library optimized for JSON DSL. Ramda-inspired, TypeScript-first.


- Complete: 200+ operators (Core, List, Object, Logic, Math, String, Type, Advanced)
- Lightweight: ~7kb bundle (vs ~50kb Ramda)
- TypeScript-first: Strong type inference
- Async native: pipeAsync, composeAsync, mapAsync
- JSON DSL ready: Designed for { $fn: "map", args: [...] }
- Placeholder support: Skip arguments with __
- Lens support: Immutable nested updates
- Transducers: Efficient transformations
``bash`
pnpm add @statedelta-libs/operators
`typescript
import { pipe, map, filter, sum, prop } from '@statedelta-libs/operators';
// Pipe - left-to-right composition
const totalActiveItems = pipe(
items,
filter(prop('active')),
map(prop('price')),
sum
);
// With operators
import { curry, __, propEq, pick, merge } from '@statedelta-libs/operators';
const getVipDiscount = pipe(
filter(propEq('isVip', true)),
map(pick(['id', 'discount'])),
);
`
---
| Function | Description |
|----------|-------------|
| identity(x) | Returns argument unchanged |always(x)
| | Returns function that always returns x |T()
| / F() | Always true / always false |tap(fn, x)
| | Execute side-effect, return original |curry(fn)
| | Curry with placeholder support |curryN(n, fn)
| | Curry with explicit arity |partial(fn, args)
| | Partial application from left |partialRight(fn, args)
| | Partial application from right |pipe(val, ...fns)
| | Left-to-right composition |pipeBuilder(...fns)
| | Create reusable pipe |compose(...fns)
| | Right-to-left composition |pipeWith(transformer, fns)
| | Pipe with custom transformer |pipeAsync(...fns)
| | Async pipe |composeAsync(...fns)
| | Async compose |pipeK(...fns)
| | Reader/context-aware pipe |composeK(...fns)
| | Reader/context-aware compose |letIn(bindings, fn)
| | Let bindings |call(fn, ...args)
| | Call function with spread args |apply(fn, args)
| | Call function with args array |__
| | Placeholder for skipping args |
`typescript
// Pipe
pipe(5, x => x + 1, x => x * 2); // 12
// Curry with placeholder
const subtract = curry((a, b) => a - b);
const subtractFrom10 = subtract(__, 10);
subtractFrom10(15); // 5
// Let bindings
letIn(
{ doubled: x => x * 2, incremented: x => x + 1 },
({ doubled, incremented }) => doubled + incremented
)(5); // 10 + 6 = 16
// call / apply
call(add, 1, 2); // 3
apply(add, [1, 2]); // 3
call(map(toUpper), ['a']); // ['A']
`
---
| Function | Description |
|----------|-------------|
| map(fn, list) | Transform each element |filter(pred, list)
| | Keep elements matching predicate |reject(pred, list)
| | Remove elements matching predicate |reduce(fn, init, list)
| | Reduce to single value |reduceRight(fn, init, list)
| | Reduce from right |head(list)
| | First element |tail(list)
| | All except first |last(list)
| | Last element |init(list)
| | All except last |nth(n, list)
| | Element at index |take(n, list)
| | First n elements |takeLast(n, list)
| | Last n elements |takeWhile(pred, list)
| | Take while predicate true |drop(n, list)
| | Remove first n |dropLast(n, list)
| | Remove last n |dropWhile(pred, list)
| | Drop while predicate true |slice(start, end, list)
| | Slice of list |find(pred, list)
| | First match |findIndex(pred, list)
| | Index of first match |findLast(pred, list)
| | Last match |indexOf(val, list)
| | Index of value |includes(val, list)
| | Contains value? |flatten(list)
| | Flatten one level |unnest(list)
| | Alias for flatten |chain(fn, list)
| | Map then flatten |uniq(list)
| | Remove duplicates |uniqBy(fn, list)
| | Remove duplicates by key |sort(comparator, list)
| | Sort with comparator |sortBy(fn, list)
| | Sort by derived value |reverse(list)
| | Reverse list |groupBy(fn, list)
| | Group by key |concat(a, b)
| | Concatenate lists |append(val, list)
| | Add to end |prepend(val, list)
| | Add to start |update(idx, val, list)
| | Set value at index (immutable) |insert(idx, val, list)
| | Insert at position (immutable) |remove(start, count, list)
| | Remove elements by position (immutable) |adjust(idx, fn, list)
| | Transform element at index (immutable) |zip(a, b)
| | Pair elements |zipWith(fn, a, b)
| | Pair with function |zipObj(keys, values)
| | Create object from pairs |some(pred, list)
| | Any match? |every(pred, list)
| | All match? |none(pred, list)
| | None match? |length(list)
| | Count elements |sum(list)
| | Sum numbers |product(list)
| | Product of numbers |mean(list)
| | Average |median(list)
| | Median |min(list)
| | Minimum |max(list)
| | Maximum |count(pred, list)
| | Count elements matching predicate |minBy(fn, list)
| | Element with minimum derived value |maxBy(fn, list)
| | Element with maximum derived value |countBy(fn, list)
| | Count by key |range(start, end)
| | Generate range |partition(pred, list)
| | Split by predicate |splitEvery(n, list)
| | Split into chunks |indexBy(fn, list)
| | Index by key |
`typescript
// Transform
pipe(
[1, 2, 3, 4, 5],
filter(x => x > 2),
map(x => x * 2),
sum
); // 24
// Group and count
groupBy(prop('category'), products);
countBy(prop('status'), orders);
// Search
find(propEq('id', 123), users);
// Modify (immutable)
update(1, 'x', ['a', 'b', 'c']); // ['a', 'x', 'c']
insert(1, 'x', ['a', 'b', 'c']); // ['a', 'x', 'b', 'c']
remove(1, 1, ['a', 'b', 'c']); // ['a', 'c']
adjust(0, toUpper, ['a', 'b']); // ['A', 'b']
// Aggregation
count(x => x > 0, [-1, 0, 1, 2]); // 2
minBy(prop('age'), users); // user with lowest age
maxBy(prop('age'), users); // user with highest age
`
---
| Function | Description |
|----------|-------------|
| prop(key, obj) | Get property |path(path, obj)
| | Get nested property |propOr(def, key, obj)
| | Get property with default |pathOr(def, path, obj)
| | Get nested with default |props(keys, obj)
| | Get multiple properties |pluck(key, list)
| | Extract property from each |has(key, obj)
| | Has own property? |hasPath(path, obj)
| | Has nested path? |objOf(key, val)
| | Create single-key object |assoc(key, val, obj)
| | Set property (immutable) |assocPath(path, val, obj)
| | Set nested (immutable) |dissoc(key, obj)
| | Remove property |dissocPath(path, obj)
| | Remove nested property |pick(keys, obj)
| | Select properties |omit(keys, obj)
| | Exclude properties |merge(a, b)
| | Shallow merge |mergeDeep(a, b)
| | Deep merge |mergeWith(fn, a, b)
| | Merge with conflict resolver |evolve(transforms, obj)
| | Transform properties |applySpec(spec, obj)
| | Build object from spec |extend(spec, obj)
| | Add computed properties |keys(obj)
| | Get keys |values(obj)
| | Get values |entries(obj)
| | Get entries |fromEntries(entries)
| | Create from entries |invert(obj)
| | Swap keys/values |
`typescript
// Access
prop('name', user); // 'John'
path(['address', 'city'], user); // 'NYC'
pathOr('N/A', ['address', 'zip'], user);
// Check
has('name', user); // true
hasPath(['address', 'city'], user); // true
// Create
objOf('name', 'John'); // { name: 'John' }
// Transform (immutable)
assocPath(['settings', 'theme'], 'dark', user);
evolve({ age: inc, name: toUpper }, user);
// Select
pick(['id', 'name'], user);
omit(['password'], user);
// Merge
merge(defaults, userSettings);
mergeDeep(baseConfig, overrides);
mergeWith((a, b) => a + b, { x: 1 }, { x: 2 }); // { x: 3 }
// Invert
invert({ a: '1', b: '2', c: '1' }); // { '1': ['a', 'c'], '2': ['b'] }
`
---
| Function | Description |
|----------|-------------|
| equals(a, b) | Deep equality |identical(a, b)
| | Reference equality |gt(a, b)
| | Greater than |gte(a, b)
| | Greater than or equal |lt(a, b)
| | Less than |lte(a, b)
| | Less than or equal |not(x)
| | Boolean not |and(a, b)
| | Boolean and |or(a, b)
| | Boolean or |both(f, g)
| | Both predicates true |either(f, g)
| | Either predicate true |complement(pred)
| | Negate predicate |allPass(preds)
| | All predicates pass |anyPass(preds)
| | Any predicate passes |ifElse(pred, onTrue, onFalse)
| | Conditional |when(pred, fn)
| | Apply if true |unless(pred, fn)
| | Apply if false |cond(pairs)
| | Switch-like conditional |tryCatch(fn, handler)
| | Try/catch wrapper |propEq(key, val, obj)
| | Property equals? |propSatisfies(pred, key, obj)
| | Property satisfies? |pathEq(path, val, obj)
| | Path equals? |pathSatisfies(pred, path, obj)
| | Path satisfies? |where(spec, obj)
| | Match spec |whereEq(spec, obj)
| | Match values |is(Ctor, val)
| | Instance check |isNil(x)
| | Is null/undefined? |isEmpty(x)
| | Is empty? |isNotNil(x)
| | Is not null/undefined? |isNotEmpty(x)
| | Is not empty? |defaultTo(def, val)
| | Default for nil |coalesce(vals)
| | First non-nil from array |
`typescript
// Predicates
filter(propEq('status', 'active'), items);
filter(where({ age: gte(__, 18), verified: equals(true) }), users);
// Conditionals
ifElse(
propEq('role', 'admin'),
always(fullAccess),
always(limitedAccess)
)(user);
cond([
[propEq('type', 'A'), handleA],
[propEq('type', 'B'), handleB],
[T, handleDefault]
])(item);
`
---
| Function | Description |
|----------|-------------|
| add(a, b) | Addition |subtract(a, b)
| | Subtraction |multiply(a, b)
| | Multiplication |divide(a, b)
| | Division |modulo(a, b)
| | Remainder (JS %) |mathMod(a, b)
| | Mathematical modulo (always >= 0) |negate(x)
| | Negate number |inc(x)
| | Increment |dec(x)
| | Decrement |clamp(min, max, val)
| | Clamp to range |abs(x)
| | Absolute value |round(x)
| | Round |floor(x)
| | Floor |ceil(x)
| | Ceiling |pow(base, exp)
| | Power |
`typescript
// Math operations
map(multiply(2), [1, 2, 3]); // [2, 4, 6]
clamp(0, 100, value);
pipe(prices, map(multiply(1.1)), map(round));
// modulo vs mathMod
modulo(-1, 4); // -1 (JS remainder)
mathMod(-1, 4); // 3 (mathematical modulo, always non-negative)
`
---
| Function | Description |
|----------|-------------|
| toUpper(str) | Uppercase |toLower(str)
| | Lowercase |trim(str)
| | Trim whitespace |split(sep, str)
| | Split string |join(sep, list)
| | Join list |replace(pattern, replacement, str)
| | Replace |startsWith(prefix, str)
| | Starts with? |endsWith(suffix, str)
| | Ends with? |test(regex, str)
| | Test regex |match(regex, str)
| | Match regex |substring(start, end, str)
| | Substring |padStart(len, char, str)
| | Pad start |padEnd(len, char, str)
| | Pad end |concatStr(a, b)
| | Concatenate strings |capitalize(str)
| | First letter uppercase |uncapitalize(str)
| | First letter lowercase |camelCase(str)
| | Convert to camelCase |snakeCase(str)
| | Convert to snake_case |kebabCase(str)
| | Convert to kebab-case |pascalCase(str)
| | Convert to PascalCase |words(str)
| | Extract words (split by whitespace) |lines(str)
| | Split by newlines |unlines(list)
| | Join by newlines |truncate(len, str)
| | Truncate with ellipsis |isBlank(str)
| | Empty or whitespace only? |template(tpl, data)
| | String interpolation {key} |charCodeAt(n, str)
| | Char code at position |fromCharCode(n)
| | Char from code |
`typescript
// Case conversion
capitalize('hello'); // 'Hello'
camelCase('hello_world'); // 'helloWorld'
snakeCase('helloWorld'); // 'hello_world'
kebabCase('HelloWorld'); // 'hello-world'
pascalCase('hello_world'); // 'HelloWorld'
// Text processing
words(' hello world '); // ['hello', 'world']
lines('a\nb\nc'); // ['a', 'b', 'c']
unlines(['a', 'b', 'c']); // 'a\nb\nc'
truncate(10, 'Hello World!'); // 'Hello W...'
// Validation
isBlank(' '); // true
isBlank('hello'); // false
// Interpolation
template('Hello {name}!', { name: 'World' }); // 'Hello World!'
// Char codes
charCodeAt(0, 'A'); // 65
fromCharCode(65); // 'A'
// Pipe example
pipe(' Hello World ', trim, toLower, split(' '), map(capitalize), join(' '));
// 'Hello World'
`
---
| Function | Description |
|----------|-------------|
| toString(x) | Convert to string |toNumber(x)
| | Convert to number |toBoolean(x)
| | Convert to boolean |toArray(x)
| | Convert to array |toObject(x)
| | Convert to object |type(x)
| | Get type name |
---
#### Lens
Immutable nested updates.
| Function | Description |
|----------|-------------|
| lens(getter, setter) | Create lens |lensProp(key)
| | Lens for property |lensPath(path)
| | Lens for nested path |lensIndex(i)
| | Lens for array index |view(lens, obj)
| | Get via lens |set(lens, val, obj)
| | Set via lens |over(lens, fn, obj)
| | Transform via lens |
`typescript
const nameLens = lensProp('name');
const addressCityLens = lensPath(['address', 'city']);
view(nameLens, user); // 'John'
set(nameLens, 'Jane', user); // { name: 'Jane', ... }
over(nameLens, toUpper, user); // { name: 'JOHN', ... }
`
#### Async
| Function | Description |
|----------|-------------|
| mapAsync(fn, list) | Parallel async map |mapSerial(fn, list)
| | Sequential async map |filterAsync(pred, list)
| | Async filter |reduceAsync(fn, init, list)
| | Async reduce |
`typescript`
await mapAsync(fetchUser, userIds);
await mapSerial(processInOrder, items);
#### Transducers
Efficient transformations without intermediate arrays.
| Function | Description |
|----------|-------------|
| mapT(fn) | Map transducer |filterT(pred)
| | Filter transducer |takeT(n)
| | Take transducer |dropT(n)
| | Drop transducer |composeT(...xforms)
| | Compose transducers |transduce(xform, reducer, init, list)
| | Apply transducer |into(to, xform, from)
| | Transduce into collection |
`typescript
const xform = composeT(
filterT(x => x > 2),
mapT(x => x * 2),
takeT(3)
);
transduce(xform, (acc, x) => acc + x, 0, [1, 2, 3, 4, 5, 6, 7]);
`
---
All pure functions are available as a single scope object, useful when you need to pass operators as a Record (e.g., for JSON DSL evaluation):
`typescript
import { scope } from '@statedelta-libs/operators';
// scope contains all 200+ functions, no placeholders or non-function values
// Type-safe: satisfies Record
// Use with DSL engines that expect a function-only scope
evaluate(expression, scope);
// Or pick what you need
const { map, filter, sum } = scope;
`
> Note: __ (placeholder) and isPlaceholder are not included in scope since they are not functions. Import them directly when needed.
---
Full type inference throughout:
`typescript
import { pipe, map, filter, prop, sum } from '@statedelta-libs/operators';
// Types flow through pipe
const result = pipe(
[{ price: 10, active: true }, { price: 20, active: false }],
filter(prop('active')), // { price: number, active: boolean }[]
map(prop('price')), // number[]
sum // number
);
// Curried functions preserve types
const getPrice = prop('price'); //
`
---
| Build | Size |
|-------|------|
| ESM | ~7kb |
| CJS | ~7kb |
| Types | ~22kb |
| Aspect | Ramda | @statedelta-libs/operators |
|--------|-------|------------------------|
| Functions | 280+ | 200+ |
| Bundle | ~50kb | ~7kb |
| TypeScript | Weak types | Strong inference |
| Async | Not native | Native support |
| Lens | Yes | Yes |
| Transducers | Yes | Yes |
| String utils | Basic | Extended (case, template, etc.) |
| JSON DSL | No | Native (scope, call, apply`) |
---
MIT