Custom type checking - isNaN vs Number.isNaN
March 22, 2019
Author: Dave Cohen
Javascript is infamous for being “loose” and misleading with its typing. typeof [1, 2, 3]
gives you 'object'
, typeof null
gives you 'object'
, etc.
So instead of writing conditionals using Array.isArray()
, doing specific null
checks, etc, I decided to make my own wrapper for typeof
which I called getType
:
/**
* Get typeof item with a few extra types specified.
* @param {any} item
* @returns {string} 'array'|'null'|'NaN'| typeof item
*/
function getType(item) {
if (Array.isArray(item)) {
return 'array';
}
if (item === null) {
return 'null';
}
if (isNaN(item)) {
return 'NaN';
}
return typeof item;
}
I unknowingly put a bug in my app. Can you spot it?
…
It’s in the 3rd if
statement.
Basically, the function took anything that wasn’t an ‘array’ or ‘null’ and decided it was ‘NaN’! The function never made it to the final return line, so I wasn’t getting ‘object’, ‘string’, ‘number’, etc like I expected.
When I discovered the issue, I wrote tests. (Yet another example of why tests are important.)
describe('getType', () => {
it('behaves as expected', () => {
const typesToTest = [
{ actual: [], expected: 'array' },
{ actual: {}, expected: 'object' },
{ actual: 'this is a string', expected: 'string' },
{ actual: 123, expected: 'number' },
{ actual: NaN, expected: 'NaN' },
{ actual: null, expected: 'null' },
{ actual: undefined, expected: 'undefined' },
{ actual: () => {}, expected: 'function' },
];
typesToTest.forEach(testObj => {
const { actual, expected } = testObj;
expect(getType(actual)).to.equal(expected);
});
});
});
I discovered two confusing things:
typeof NaN
gives you'number'
isNaN('NaN')
,isNaN(undefined)
,isNaN({})
,isNaN('blabla')
are alltrue
!
I just needed to change one line:
...
if (typeof item === 'number' && isNaN(item)) {
return 'NaN';
}
...
Later I discovered the section on Number.isNaN
in the Airbnb JavaScript Style Guide:
Verbatim:
The Standard Library contains utilities that are functionally broken but remain for legacy reasons.
- 29.1 Use
Number.isNaN
instead of globalisNaN
.
eslint: no-restricted-globals
Why? The global
isNaN
coerces non-numbers to numbers, returning true for anything that coerces to NaN. If this behavior is desired, make it explicit.
// bad
isNaN('1.2'); // false
isNaN('1.2.3'); // true
// good
Number.isNaN('1.2.3'); // false
Number.isNaN(Number('1.2.3')); // true
Read more: Number.isNaN() - JavaScript | MDN
In light of learning that, I could remove the typeof item === 'number'
check. Here’s the fully working function:
/**
* Get typeof item with a few extra types specified.
* @param {any} item
* @returns {string} 'array'|'null'|'NaN'| typeof item
*/
function getType(item) {
if (Array.isArray(item)) {
return 'array';
}
if (item === null) {
return 'null';
}
if (Number.isNaN(item)) {
return 'NaN';
}
return typeof item;
}
Short and sweet!
One way to use getType
:
const thing = [1, 2, 3];
const thingType = getType(arr);
if (['array', 'object', 'number'].includes(thingType)) {
runAwesomeProcess(thing);
} else {
throw new Error(
`Bad input: ${thing}. Expected array, object, or number, got '${thingType}'`
);
}
Compare that to:
// if thing is not falsy (null check) and typeof thing is object (array or object)
// or thing is a number that isn't NaN
if (
(!!thing && typeof thing === 'object') ||
(typeof thing === 'number' && !isNaN(thing))
) {
runAwesomeProcess(thing);
} else {
throw new Error(
`Bad input: ${thing}. Expected array, object, or number, got '${typeof thing}'`
);
}