If you have read my post, Dev Retro 2022: What I learned in my 43rd year..., you know I've been in the software development game for a long time. However, it's only recently -- the last three years or so -- that I have used Javascript and Typescript professionally -- and exclusively -- in my role as a full-stack MERN engineer. (Note: Hereinafter, when I mention Javascript or JS, know that I'm including Typescript (TS) unless I clearly state otherwise.)
There are days when I love JS/TS. The language's rich prototypes for objects and arrays; the wealth of utilitarian methods available on those prototypes; the economy of expression that is possible in one's code; the freedom to use Type-based code (ES6+/Typescript) when it serves a REAL purpose; the freedom to eschew type-based code when it becomes a limiting factor. All of these aspects of the language are useful and, perhaps, are among the reasons that Javascript has become the language of choice for many Web apps both on the client and server side.
...but "da devil be lurkin' in dem details..."
This particular devil bit me this week. I managed to pollute the Array
constructor in one mistyped line of code that brought testing to a halt for several hours while a handful of extremely experienced engineers -- including me -- puzzled over an error being thrown deep inside the code of a third-party package: Array.isArray is not a function
.
It took several hours to identify the culprit -- an errant line of code in the app itself. Here's what the code looked like, essentially.
function wasWorkingUntilRecentChanges(...args) {
// do some stuff for 10-15 lines of high-density code
const thisArray = Array<Record<string,any>> =
thatArray.filter((i) => i.property);
// do some more stuff for 10-15 lines of high-density code
// Suddenly, this line of code has begun throwing an Error
// with the message `Array.isArray is not a function`
const myResult = em.find(MyType, {});
Do you see it? I suppose the problem might be incredibly obvious here since I have elided the surrounding code that obfuscated the mistake.
Example 1 - The simple line of code that should have been
const thisArray: Array<Record<string,any>> =
thatArray.filter((i) => i.property);
This was what was intended -- declaring an array with a specific type to catch the result of thatArray.filter()
.
Example 2 - The similar-looking, though monstrously fateful line of code that was
const thisArray = Array<Record<string,any>> =
thatArray.filter((i) => i.property);
This was the actual code that was committed -- a multiple assignment statement that polluted (over-wrote) the Array
constructor object.
Note that the damage caused here didn't result in any errors or warnings from Javascript, Typescript or ESLint in VS Code. They all silently approved.
There was no complaint from the Typescript compiler when the code was built.
Only when the code (a CLI) was run in Node.js did the monstrous result become manifest: Array.isArray is not a function
.
Wha...?!?!?!
Array
is the constructor function for every array in Javascript (see Array on MDN). It is provided as part of the Javascript engine in the execution environment -- Node.js, the Browser, etc. When you write const myArray = [];
The Array
constructor is used to create that object.
In addition, both Array
and Array.prototype
provide a number of utilitarian methods for handling arrays including Array.isArray()
; the ubiquitous methods Array.prototype.map()
and Array.prototype.filter()
; and a host of others.
However, Array
is not an immutable (read-only) object because there is no such thing in Javascript. In fact, the JS Array
function is just like any object in Javascript. It is writable. Herein lies the problem.
So, the perfectly legal, mutiple-assignment statement in Example 2 runs afoul of these JS "features". Here's what happens.
thatArray.filter(...)
is called.The result of the function call is stored in
Array<Record<string, any>>
which actually meansArray
.The value stored in
Array
(i.e., now the value returned by the function) is stored inthisArray
.
The net result here is that the Array
function has been obliterated (over-written) by the results of thatArray.filter(...)
. In this case, that was just a POJO from an imported .json
file.
The consequence is that, from this point forward, any JS code in this routine -- including code in packages -- that attempts to use Array
in its official capacity will fail.
const myArray = []
will fail because it attempts to call theArray
function behind the scenes.Array.isArray(myObj)
will throwArray.isArray is not a function
because... well... it is no longer a function.myArray.map((a) => ({ ...a, ...b})
will continue to work because these functions are onArray.prototype
which is a different object thanArray
and any existingArray
already has a reference toArray.prototype
in it's own.prototype
property.
The truly heinous thing here is that the problem may not show up immediately. Until the program reaches a line of code that must create an array or check whether an object is an array, the devastation will be buried.
...and when the program runs headlong into this pit of devastation -- which it almost certainly will -- the error messages, though helpful, will be confusing. "How can Array.isArray not be a function? It's always a function..."
Well, until it's not. ๐
If this happens to you, go looking for something like Example 2 above in your own code -- not in the package that threw the error.
Your first instinct might be to search for this error message related to the package that threw it. I did... for several hours... which was both fruitless and frustrating.
Remember. The package that threw the error is an innocent victim. Your own code is the villain.