Dynamically Filter Array of Objects in Typescript
Jasser Mark Arioste
In one of my previous articles, I showed you how to sort an array of objects dynamically. In this article, you will learn how to filter an array of objects in typescript dynamically by creating dynamic expressions.
Introduction #
First, we'll start off with the basics since some applications don't need a very sophisticated filtering logic. Then we'll gradually move to the more advanced filtering logic that uses dynamic queries that mimics MongoDB Queries.
By the end of this tutorial, you'll be able to filter arrays however you'd like. Also, note that we will be using Typescript throughout the tutorial.
Basic Filtering #
Bobbyhadz has a tutorial that covers how to filter an array using multiple conditions. Basically, when filtering an array, you use a predicate function against an array element. This predicate function must return a boolean
value and if the result is true, the element is included in the results. For example:
const people = [ {name: 'Adam', age: 35}, {name: 'Bob', age: 41}, {name: 'Carl', age: 30}, {name: 'Dennis', age: 52}, ]; // filter all people whose age is greater than or equal to 40. const results = people.filter(person => { return person.age >= 40; }); console.log(results) // [{name: 'Bob', age: 41}, {name: 'Dennis', age: 52}]
12345678910111213
Inside the predicate function, you can add as many conditions as you'd like:
const results = people.filter(person => { return person.age >= 40 || person.age <= 30; });
123
That's all good. But, what's the downside of coding it this way?
In a real-world application, sometimes hardcoding the predicate function is not enough. We need a more dynamic way to specify the filter.
What if we want different filter conditions? We don't know this at compile-time. This is true if the filters come from a user input.
In the next section, we'll learn how to add a bit of dynamic fuel to our predicate function.
Filter Using Dynamic Expressions #
First, let's start from a user perspective. If you're a user of an app, what do you want when filtering? You would want the property/key, operation, and values to be dynamic, as well as the number of expressions to be dynamic. Suppose you want to "filter all users with age greater than 40".
If we translate this to code it would be something like this:
const expressions = [ { key: "age", operation: "greater_than", value: 40 }]
123456
Looks good, we have two expressions that correspond to what we want above.
Another example would be, filter all the users whose first_name starts with the letter A.
Translating this into code would be:
const expressions = [ { key: "first_name", operation: "starts_with", value: "A" } ]
1234567
Now, let's use this expressions
array to create our predicate function.
First let's define a type to represent an expression or a filter condition:
type BasicExpression = { key: string; operation: "greater_than" | "less_than" | "equal" | "starts_with" | "contains", value: string | number; }
12345
Now, how do we evaluate BasicExpression
? Let's create a utility function to evaluate if an expression is true
or false
:
function evaluateExpression(expression: BasicExpression, obj: any): boolean { const { key, operation, value } = expression; const propValue = obj[key] switch (operation) { case "greater_than": return propValue > value; case "less_than": return propValue < value; case "contains": return new RegExp(value + "").test(propValue + "") case "starts_with": return new RegExp("^" + value + "").test(propValue + "") case "equal": default: return propValue === value; } }
12345678910111213
Explanation:
Line 1: We pass in an expression
and the obj
Object it's evaluated against.
Line 2: We destructure the expression for readability.
Line 3: We get the value that we're evaluating against.
Lines #4-12: We check the operation
and evaluate the object's property value against the expression's value.
For example, If we wanted to use it on a single object it would be like this:
const user = { name: "Adam", age: 30 } const expression:BasicExpression = { key: "age", operation: "less_than", value: 40 } const result = evaluateExpression(expression, object) console.log(result) //true
12345678910111213
If we wanted to use it on an array, it would be like this:
const people = [ {name: 'Adam', age: 35}, {name: 'Bob', age: 41}, {name: 'Carl', age: 29}, {name: 'Dennis', age: 52}, ]; const expressions:BasicExpression[] = [ { key: "age", operation: "less_than", value: 30 }, { key: "age", operation: "greater_than", value: 40 } ] // or operation, use .some() const result = people.filter(item => expressions.some(expr => evaluateExpression(expr, item))) console.log(result.length) //3 // and operation, use .every() const _result = people.filter(item => expressions.every(expr => evaluateExpression(expr, item))) console.log(result.length) //0
1234567891011121314151617181920212223242526
Here's a stackblitz demo for both examples.
And with that, we can now filter dynamically! However, it's not over yet.
You might have noticed that we're using .some()
and .every()
methods if we wanted to simulate "and"
or "or"
operators. This is currently hard coded right now and that's a problem. Also, the expressions we can create with this technique is very simple.
What if we want to filter: all users with a name that starts with A or B and an age greater than 40? This is impossible right now.
In the next section, we'll finally create Query objects that integrate boolean
expressions to handle this scenario.
Implementing Logical Operators #
How do we model the above query? In code, I would model it like this:
const query = { condition: "and", expressions: [ { key: 'age', operation: 'greater_than', value: 40, }, { condition: "or", expressions: [ { key: 'name', operation: 'starts_with', value: 'A' }, { key: 'name', operation: 'starts_with', value: 'B' } ] } ] }
12345678910111213141516171819202122232425
You may have noticed that it's like a tree data structure:
result = and |- age greater than 40 | - or |- name starts with A |- name starts with B
123456
In Typescript, we define this tree-type structure like this:
type BasicExpression = { key: string; operation: "greater_than" | "less_than" | "equal" | "starts_with" | "contains", value: string | number; } type LogicalExpression = { condition: "and" | "or", expressions: Expression[] } type Expression = BasicExpression | LogicalExpression
12345678910111213
Evaluating a LogicalExpression
is a whole different beast than evaluating a BasicExpression
. Why? Because it can contain a LogicalExpression
inside it as well. To evaluate a LogicalExpression
, you have to traverse each node like a tree and you have to use recursion for this.
Let's create a utility function:
function evaluateLogicalExpression(expr: LogicalExpression, obj: Object): boolean { const { condition, expressions } = expr; const fn = condition == "and" ? expressions.every : expressions.some; return fn.call(expressions, (expr) => { const isQuery = "condition" in expr; if (isQuery) { return evaluateLogicalExpression(expr, obj) } else { return evaluateExpression(expr, obj); } }) }
123456789101112
Explanation:
Line 1: Similar to our evaluateExpression
, we pass in a LogicalExpression
and an Object
that it should be evaluated against.
Lines 2-3: We check if the condition is "and"
, and we use the Array.every()
method. Otherwise, use the Array.some()
method.
Line 4: We evaluate all expressions
Line 5-8: Check if it's a logical expression or a basic expression, If it's a logical expression we use recursion and call evaluateLogicalExpression
to get the result for that sub-tree. Otherwise, we use evaluateExpression
to evaluate the basic operations.
Enough talk, let's see how it works!
const expr: LogicalExpression = { condition: 'and', expressions: [ { key: 'age', operation: 'greater_than', value: 40, }, { condition: 'or', expressions: [ { key: 'name', operation: 'starts_with', value: 'A', }, { key: 'name', operation: 'starts_with', value: 'B', }, ], }, ], }; const people = [ { name: 'Elsworth', age: 41 }, { name: 'Xenos', age: 45 }, { name: 'Debra', age: 90 }, { name: 'Allen', age: 4 }, { name: 'Malissia', age: 18 }, { name: 'Patric', age: 55 }, { name: 'Minni', age: 84 }, { name: 'Alberik', age: 63 }, { name: 'Euell', age: 10 }, { name: 'Barton', age: 27 }, ]; const result = people.filter((user) => evaluateLogicalExpression(expr, user)); console.log(result) //[{ name: 'Alberik', age: 63 }]
1234567891011121314151617181920212223242526272829303132333435363738394041
Now, Everything is dynamic in the form of a LogicalExpression
.
Conclusion #
Depending on your application requirements, filtering arrays in JavaScript can be as easy as pie or as complex as creating functions that parse logical expressions in the form of a tree data structure.
All in all, we learned how to completely create a dynamic query for the Array.filter()
function removing any hard-coded values.
Hopefully, you enjoyed learning in this tutorial as much as I did making it. If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.
What's Next? #
Depending on the size of your application, you might need to add more operators like "before_date
" or "after_date"
, etc. In this tutorial, I used objects to define the LogicalExpression
tree. But if you look closely, you could make it more concise by transforming it into Tuples.
In the next tutorial, I'll create a simple react application that uses this technique for filtering arrays.
Resources #
- Full code and Demo are available on StackBlitz: How to Filter Dynamically in Typescript
- Javascript Recursion Guide
- Array.filter() docs
Credits: Image by Volker Glätsch from Pixabay