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 1
s
>> data.filter(EQ(1))
Array(3) [ 1, 1, 1 ]
Get All the 1
s and 2
s
>> 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
AND
andOR
on firstfalse
ortrue
respectively instead of iterating over the entire array of object entries.Support implicit
EQ
for non-predicate-function object spec values, e.g.{ isAdmin: true }
would be interpreted as{ isAdmin: EQ(true) }
.
Top comments (0)