What I Learned at Work this Week: Is null an object?

Photo by Felix Mittermeier from Pexels

By now, we’re all familiar with the TypeScript codebase I use at work. The code relies on a lot of abstraction and I’m getting better at using it without having to figure out how every piece works. But when I notice unexpected behavior, I have to be prepared to use what I know about TypeScript and JavaScript to dig deep into the code. This week, I wrote a configuration that worked just fine in certain circumstances, but threw this error in others:

This is a recreation, but it was the same error

Debugging this error led me down a path that made me question what I knew about JS: is null an object?

The DevTools console is really helpful for exposing errors and linking the offending line of code. I still often find myself searching, though, because when I test my code in the browser, it has to be transpiled and is therefore minified or at least merged into a single file. When this happens, I progressively place debuggers deeper into the code until eventually I hit the error before I hit a debugger. That led me to this line:

} else if (typeof target === 'object') {
const keys = Object.keys(target);

When I had a debugger above this line, target was null. But then when I stepped forward, the condition was met and the error was thrown. I had to see this for myself to be sure:

…what?

Well this is strange. What’s the story?

In JavaScript, we can use the typeof operator to return a string describing the data type of an input:

As we could see in my previous screenshot, when running typeof on null, our return is an object. According to W3 Schools, this is a bug in the language and that the type should be null. This makes sense since objects have certain functions and abilities that null doesn’t have. Objects, for example, can be assigned properties. What happens if we try to assign a property to null?

We run into errors when we try to treat null like an object, which is exactly what happened to me at work. The target variable had previously been assigned a null value as a default and I believe the author’s intent was that it would not trigger the conditional logic, which inspects the keys of the target object and performs an operation on a different object based on the inputs therein. To iterate through the keys of our object, we use the keys method associated with JavaScript’s Object class. It produces a handy array:

It even works if there aren’t any keys!

Object.keys expects an object as an argument. The error we see here supports the assertion that the typeof null result is a bug. Object.keys needs an object to work, so when it receives null, it complains. Further adding to the confusion, it’s actually possible to create something called a “null object,” which is an object (but not an instance of Object):

If we use Object’s create method and pass in an argument of null, we’ll create something that looks like an object, but doesn’t inherit properties from the Object class. According to MDN, this is useful for debugging because common object-property converting/detecting utility functions may generate errors, or lose information. As we can see, native functions like toString don’t exist in our null object, but it’s still a true object — it can be assigned properties and it works with Object methods like keys.

There’s nothing we can do about the core JS code, but there is a really easy workaround in this case. The code now reads:

} else if (target !== null && typeof target === ‘object’) {
const keys = Object.keys(target);

By adding that condition, our null value would never be placed in Object.keys and never throw the error. We might have also considered changing the default type of target to undefined instead of null. There’s no typeof bug with undefined, but I preferred not to switch the type because it could have caused an unexpected side effect in another part of the code. One remaining mystery is how this could have happened in the first place. Check out the first line of the function that calls this logic:

function processTarget(cfg: ValuesTransformerConfig, target: undefined | string | number | AnalyticsData) {

Note that target isn’t assignable to a null data type. None of the types listed, including undefined, allow for a variable to be defined as null (AnalyticsData is a custom interface, but it’s an object with a bunch of optional properties). It’s smart to recognize that the original logic wouldn’t have worked if target was null, but for some reason it’s still allowed despite this setting.

This wasn’t exactly the most complex problem to solve, but it’s always exciting when we can learn something new about a language and apply what we know to fix a bug. If you love JavaScript like me, then you love it as it is. Quirks are what makes it fun!

Solutions Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store