2

I am building a REST API in NodeJS. I am building the server-side pagination, sorting and filtering.

I have a simple implementation of the filter method. This works if the item does not contain any empty strings. However if the item contains an empty string, the .toString() method fails because item[filterBy] is null.

Cannot read property 'toString' of null

How can I update the filter function to simply return true if the item contains an empty string, to skip the comparison operations?

  // Apply filter to array of objects
  // TODO: Empty strings breaks the filter
  items = items.filter(item =>
    item[filterBy].toString().trim().toLowerCase().indexOf(filterValue.toLowerCase()) !== -1)

5 Answers 5

2

To skip the item contains an empty string, try this:

item[filterBy] && item[filterBy].toString()

So, your code will be:

items = items.filter(item =>
item[filterBy] && item[filterBy].toString().trim().toLowerCase().indexOf(filterValue.toLowerCase()) !== -1)
Sign up to request clarification or add additional context in comments.

Comments

2

Perhaps something like this:

const filterValue = 'abc'
const filterBy = 'name'
const items = [
  {x: 1, name: 'abc'},
  {x: 2, name: 'def'},
  {x: 3, noname: 'def'},
  {x: 4, name: 'ABCDEF'},
]

const newItems = items.filter(item =>
    !(filterBy in item) || item[filterBy].toString().trim().toLowerCase().indexOf(filterValue.toLowerCase()) !== -1)
    
console.log(newItems)

All we do here is check if the item has a filterBy property.

It's not quite clear to me what the issue is with empty strings. It seems most likely that you would get that error if item[filterBy] is undefined. That's what we check here.

If instead you want to skip those that don't have the relevant property, switch from !(filterBy in item) || ... to (filterBy in item) && ....

Comments

1

Maybe:

items = items.filter(
    item => item[filterBy] && item[filterBy].toString ?
        item[filterBy].toString().trim().toLowerCase().indexOf(filterValue.toLowerCase()) !== -1 :
        false
)

Comments

1

flatMap() is .map() and .flat() combined. It can act like a reverse .filter() by directly returning or not returning values. See demo for details.

const items = [{
  id: 'a',
  pn: 'B',
  desc: ''
}, {
  id: 'b',
  pn: 'c',
  desc: 'd'
}, {
  id: 'c',
  pn: 'k',
  desc: null
}, {
  id: 'i',
  pn: 'k',
  desc: 2
},, {
  id: 'o',
  pn: 'x',
  desc: 3
}];

// Utility function that returns a 'friendlier' value
function clean(obj) {
  return obj == null ? false : Array.isArray(obj) ? obj : obj.constructor.name.toLowerCase() === "object" ? obj.toString().trim().toLowerCase() : typeof obj === "string" ? obj.toString().trim().toLowerCase() : Number(parseFloat(obj)) === obj ? obj : false;
}

// Filters by a single key and multiple values
let filterKey = clean('desc');
let filterVal = clean(['d', 2, 'f']);

/* It returns each value in a sub-array an empty array will remove
the value. In the final stage it flattens the array of arrays into a normal array
*/
let list = items.flatMap(item => {
  return Object.entries(item).flatMap(([key, value]) => filterKey === (clean(key)) && [...filterVal].includes(clean(value)) ? [clean(value)] :[]);
});

console.log(list);

3 Comments

How does that improve on let list = items.filter(item => filter.includes(item))?
You can chain ternaries and in one line settle as many conditions as needed. So if you want to return something other than the value it's really simple and you can specifically remove whatever you like no problem. ATM the requirements are simple but is it wrong to show an alternative way?
Oh, it's clearly an interesting technique, a nice way to combine map and filter in one go. But as this isn't a direct answer of the question, if you're going to show this technique, I think it would be much better to show it doing something useful. Your sample is written more simply with items.filter. If you changed to ... ? [items.foo] : [], I think it would be much more clear.
0

It is correct to chain filters and it does not affect performance.

const filterValue = 'John'
const filterBy = 'name'
const items = [
  {id: 1, name: 'John'},
  {id: 2, name: 'Doe, John'},
  {id: 3, ref: 1},
  {id: 4, name: 'No one'},
  {id: 5, name: null}
]

let fItems = items.filter(item => item[filterBy])
                  .filter(item => item[filterBy].toString().trim().toLowerCase()
                     .indexOf(filterValue.toLowerCase()) !== -1)
   
console.log(fItems);

Update: fixed the code.

2 Comments

This is not a filter -- you're changing the data inside map, not filtering it.
Thank you, @ScottSauyet. Updated my answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.