I recently wanted to query arrays of Javascript objects using a declarative syntax and happily took the opportunity to write some small functions.
Predicates
EQ (is equal to)
const EQ = x => y => x === y
For anyone unfamiliar with ES6 arrow functions:
EQ is a function that accepts one argument (x) that returns another function that accepts one argument (y) which returns the result of evaluating x === y.
Here's the equivalent standard function def:
function EQ (x) {
return function (y) {
return x === y
}
}
IN (is included in)
const IN = (...xs) => x => xs.includes(x)
IN is a function that accepts one or more arguments collected into an array (xs) that returns another function that accepts one argument (x) which returns the result of evaluating xs.includes(x).
Logical Operators
const NOT = pred => x => !pred(x)
const AND = (...preds) => x => preds.reduce((acc, pred) => acc && pred(x), true)
const OR = (...preds) => x => preds.reduce((acc, pred) => acc || pred(x), false)
Doing Stuff
Filtering Scalar Arrays
const data = [ 1, 2, 1, 1, 3, 2, 2, 2 ]
Get All the 1s
>> data.filter(EQ(1))
Array(3) [ 1, 1, 1 ]
Get All the 1s and 2s
>> data.filter(IN(1, 2))
Array(7) [ 1, 2, 1, 1, 2, 2, 2 ]
>> data.filter(OR(EQ(1), EQ(2)))
Array(7) [ 1, 2, 1, 1, 2, 2, 2 ]
Filtering Arrays of Objects
The above EQ and IN predicate functions work great with scalar values (i.e. numbers, booleans, etc), but I needed something that works on objects:
const OBJ = spec => obj => Object.entries(spec).reduce((acc, [k, pred]) => acc && pred(obj[k]), true)
OBJ accepts an object-type spec argument that maps key names to predicates.
For example, a spec value of:
{ isAdmin: EQ(true), active: EQ(true) }
would match objects with isAdmin = true AND active = true. For performing logical ops other than AND, you can specify them separately and wrap them appropriately. For example, to do an OR query on these same property values:
OR( OBJ({ isAdmin: EQ(true) }), OBJ({ active: EQ(true) }) )
Better may be to create an OBJ_OR or something but...moving on
Get some legit-looking data from JSONPlaceholder
const Todos = await (await fetch("https://jsonplaceholder.typicode.com/todos")).json()
The returned array looks like:
[
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
},
...
]
Find all uncompleted Todos from Users 1 and 2:
>> Todos.filter(OBJ({userId: IN(1, 2), completed: EQ(false)}))
Array(21) [ {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, … ]
🎉
Optimizations omitted in favor of simplicity
Short-circuit
ANDandORon firstfalseortruerespectively instead of iterating over the entire array of object entries.Support implicit
EQfor non-predicate-function object spec values, e.g.{ isAdmin: true }would be interpreted as{ isAdmin: EQ(true) }.
Top comments (0)