Easily stripping “blank values” from an array
Published on 11 May 2020
• 4 min
Cette page est également disponible en français.
Welcome to the eighth article of our daily series “19 nuggets of vanilla JS.”. Need to clean up an array? We’ve got a lot of solutions, and some are… super concise!
The series of 19
Check out surrounding posts from the series:
- Short-circuiting nested loops
- Inverting two values with destructuring
- Easily stripping “blank values” from an array
- Long live numeric separators!
- Properly sorting texts
- …and beyond! (fear not, all 19 are scheduled already)…
A few reminders on filter
Since ES5 (2009), the Array
type has sported many iterative methods directly inspired by Prototype.js’ Enumerable
module:
forEach
to invoke a callback on each element (you should now favorfor…of
);map
to produce a derived array via a transform function;every
andsome
to determine whether a predicate (a yes/no function) is passed by all or some items of the array;reduce
andreduceRight
to produce a single consolidated value by traversing all values in the array (e.g. a sum, a concatenation, a hash);- Finally,
filter
to produce a derived array that only retains values that satisfied a predicate.
With the exception of reduce
and reduceRight
that accept 3, all other methods accept 2 arguments:
- The callback function (for
filter
, that would be the predicate); - The context: if the callback function expects a specific
this
but passing it by reference would “lose it” (which means it was declared usingfunction
or the shorthand method syntax), this optional argument lets us specify the correct context (which is super useful and performant).
(By the way, these callbacks get not one but three arguments: the value, its index, and the entire array. This can come in super handy…)
Here are a few examples:
function isEven(n) {
return n % 2 === 0
}
const values = [1, 1, 2, 3, 5, 8, 13, 21, 34]
values.filter(isEven) // => [2, 8, 34]
const numbers = [1, Math.PI, 42, Infinity]
numbers.filter(Number.isInteger) // => [1, 42]
“Blank values”?
“Stripping blank values,” meaning what, exactly? What is a “blank” value? Well, this will largely depend on your actual needs.
Most often, you’ll have an array of numbers (where zero is deemed invalid) or strings (where empty strings are deemed invalid). In both cases, false
, null
, NaN
and undefined
would be regarded as invalid too.
This is the super-easy case we’ll see later. But what if your situation is different? Perhaps you want zeroes? Or false
? Or you want to remove whitespace-only strings?
No worries, I’ve got a series of cute handy predicates for you.
A bunch of useful predicates
Value(s) to test for | Predicate |
---|---|
undefined |
x === undefined or typeof x === 'undefined' |
null or undefined |
x == null (note the loose equality) |
empty string ('' ) |
Use as boolean, or x === '' |
whitespace-only string | x.trim() as boolean, or x.trim() === '' |
Text convertible to number (except NaN) | Number |
“Usable” number (neither NaN nor infinite) | Number.isFinite (ES2015) |
Integer | Number.isInteger (ES2015) |
Reliable integer1 | Number.isSafeInteger (ES2015) |
1 Every Number
in JS relies on the IEEE 754 standard; these are floating-point 64-bit numbers with a maximum of 15 digits of precision. Beyond a certain limit, integers are rounded to their closest representation. Try evaluating 9999999999999999
in your console or Node REPL, and then even 1234567890123456789
…
If you’re processing texts, but want to consider falsy values (e.g. null
, undefined
, false
or NaN
) as empty strings, you can go with String(x || '')
(because String(undefined)
would result in 'undefined'
, among other things).
If you regard whitespace-only strings as empty too: String(x || '').trim()
is your friend. Examples:
const data = [
0,
1,
42,
1e100,
Infinity,
NaN,
false,
'',
' ',
'3.14',
'42',
'Infinity',
]
// Only _truthy_ values, except whitespace-only strings:
data.filter((x) => String(x || '').trim())
// => [1, 42, 1e+100, Infinity, '3.14', '42', 'Infinity']
// Only reliable integers
data.filter(Number.isSafeInteger)
// => [0, 1, 42]
// Only values convertible to “reliable” integers:
data.filter((x) => Number.isSafeInteger(Number(x)))
// => [0, 1, 42, false, '', ' ', '42'] (false turns into 0)
Need to reject instead of retaining?
Sometimes you’ve got a predicate available, that might contain some fairly complex code, and tough luck: it tests the exact opposite of what you want. It says “yes” when you want to reject and “no” when you want to retain. Alas, you only have filter
around, there is no reject
method.
Please don’t go rewriting that predicate yourself trying to invert its logic (if the predicate is complex, this can quickly break down). The easiest way is to use a negator:
function isEven(n) {
// Pretend this code is actually complex…
return n % 2 === 0
}
const values = [1, 1, 2, 3, 5, 8, 13, 21, 34]
const odds = values.filter((n) => !isEven(n))
// => [1, 1, 3, 5, 13, 21]
If you do this a lot, you can make the negation generic:
function not(fn) {
return (x) => !fn(x)
}
// Or for any signature:
function not(fn) {
return (...args) => !fn(...args)
}
const odds = values.filter(not(isEven))
As you might expect, Lodash provides this: _.negate
…
The super-easy case: Boolean
!
We mentioned earlier the rather widespread case of an array of numbers (where zeroes are invalid) or strings (where empty strings are invalid). In both cases, false
, null
, NaN
and undefined
would also be regarded as invalid.
When you want to filter out all falsy values, there’s a super-concise way:
data.filter(Boolean) // 😎
It’s sort of like the data.compact
you’d find in other languages (e.g. Ruby): it will strip out null
and undefined
, which is rather common base case, but also take out false
, NaN
and ''
, which is a rather nice bonus. OK, this will also remove 0
, so be careful if that’s an issue for you. But I do use this all the time to clean up, for instance, an array where every result item must be a non-empty text or some random object.