ReactHustle

Dynamically Filter Array of Objects in Typescript

Jasser Mark Arioste

Jasser Mark Arioste

Dynamically Filter Array of Objects in Typescript

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 #

Credits: Image by Volker Glätsch from Pixabay

Share this post!

Related Posts