@statedelta-libs/operators
v0.2.3
Published
Functional operators library optimized for JSON DSL - Ramda-inspired, TypeScript-first
Maintainers
Readme
@statedelta-libs/operators
Functional operators library optimized for JSON DSL. Ramda-inspired, TypeScript-first.
Features
- 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
Installation
pnpm add @statedelta-libs/operatorsQuick Start
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'])),
);API Reference
Core
| 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 |
// 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']List
| 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 |
// 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 ageObject
| 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 |
// 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'] }Logic
| 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 |
// 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);Math
| 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 |
// 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)String
| 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 |
// 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'Type
| 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 |
Advanced
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 |
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 |
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 |
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]);Scope
All pure functions are available as a single scope object, useful when you need to pass operators as a Record<string, Function> (e.g., for JSON DSL evaluation):
import { scope } from '@statedelta-libs/operators';
// scope contains all 200+ functions, no placeholders or non-function values
// Type-safe: satisfies Record<string, (...args: any[]) => any>
// 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) andisPlaceholderare not included inscopesince they are not functions. Import them directly when needed.
TypeScript
Full type inference throughout:
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'); // <T>(obj: T) => T['price']Bundle Size
| Build | Size | |-------|------| | ESM | ~7kb | | CJS | ~7kb | | Types | ~22kb |
Comparison with Ramda
| 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) |
License
MIT
