TypeScript: Null and Undefined Types

Trying to perform operations on undefined or null values is one of the most common causes of runtime errors in JavaScript code.

Sometimes the variables in our programs intentionally “hold” undefined or null - JavaScript is a very flexible language, and we can have legitimate reasons to want to use these values to express things in our code. Other times, we may inadvertently be interacting with the values undefined and null because we did not account for all the possible code paths, or we did not understand a third party or platform API well enough.

How can we protect ourselves, and therefore also our users, from undefined and null values appearing where we don’t want them?

TypeScript to the rescue!

Let’s take a look at an example of a very common operation within a web app: looking something up in the DOM and updating it in some way.

const myComponent = document.getElementById('component');
myComponent.innerHTML = 'updated content';

This code seems harmless enough, and running this through the TypeScript compiler with no extra configuration will not yield any errors.

However, if we think a lot more carefully about the moving parts here, the key thing is that we understand the interface that is representative of this part of the DOM API.

We know intuitively that when things are looked up in the DOM, there is no guarantee that they will be available to be found. What does getElementById() return in that case? It turns out it is the value null.

This means that if, in our code above, the element with an ID of 'component' doesn’t exist at the moment we try and look it up, the myComponent variable will be null and our program will crash when we effectively try and access null.innerHTML on the next line.

Uncaught TypeError: Cannot read property ‘innerHTML’ of null

So why on earth didn’t TypeScript warn us about this possibility?

Let’s take a look at that “interface” (specifically the signature of the function) we spoke about for getElementById(). If we hover over the method in a modern IDE which supports TypeScript, such as VSCode, we will see something similar to the following:

(method) Document.getElementById(elementId: string): HTMLElement

It looks like we have found our disconnect - at runtime there are two possible values (an HTMLElement object or null), but at compile time TypeScript believes the return value will always be of one single type, HTMLElement.

The reason for this is that, prior to version 2 of TypeScript, null and undefined were actually what’s called a “subtype” of every other type. This means that null was assignable to any other type including numbers, strings etc.

As of TypeScript 2 however, we have the concept of “non-nullable types”. This is a fancy way of saying that null has become its own unique type. It’s no longer a subtype of all the other types which means that if a variable/parameter/return type of a function could be a string or null you will have to type it as such with a union type of both of them: string | null.

The exact same thing is true for undefined under this “non-nullable” types umbrella: undefined now has its own distinct type, which is not assignable to anything else. It is also worth explicitly noting that undefined and null types are therefore also not assignable to each other.

So let’s take another look at our example with getElementById() to see what we mean by all that. Here is the simple example again:

const myComponent = document.getElementById('component');
myComponent.innerHTML = 'updated content';

This new behavior of non-nullable types is actually opt-in TypeScript version 2 and above, so we need to go into our tsconfig.json file to enable it.

All we have to do to enable this behavior is to set “strictNullChecks”: true (or rely on it being set via the umbrella “strict”: true flag) in our tsconfig.json compilerOptions. If we were to rerun our simple program through the TypeScript compiler again we would now get an error.

[ts] Object is possibly ‘null’

Hurray! That error makes perfect sense based on our understanding of what getElementById() can return.

If we hover over the method in the same way we did before we’ll see something really interesting: the function signature has actually been updated!

(method) Document.getElementById(elementId: string): HTMLElement | null

The return type is now a union type of HTMLElement or null.

In our code, we’ve so far implicitly just assumed that the result of getElementById() will be an HTMLElement which we can then use and access the innerHTML property of. However, TypeScript is now picking up on the fact that there is no guarantee that that will possible. There’s no way to know by statically analyzing this program that the element we are looking up will exist in the DOM at the time that we’re querying for it.

One of the great features of TypeScript which partners really well with these non-nullable types is the fact that it will do what’s called “control flow analysis” on our programs when it performs type checking. In simple terms this means that it will look at our if statements, our switch cases and similar kinds of constructs within our programs and know how that affects the types within our programs. Let’s see what we mean using our example.

Essentially if we put in an if statement to check that myComponent is truthy, we will see that the TypeScript error goes away. At the point at which we run our if statement, myComponent could be an HTMLElement or null. But within the body of our if statement, we know that myComponent must be truthy, and with null being a falsy value, the type of null can be removed from our union type:

const myComponent = document.getElementById('component');
// At this point `myComponent` is of type HTMLElement | null
if (myComponent) {
  // Within the block of this statement, `myComponent` must contain a truthy value,
  // so the `null` type can be removed from the union type.
  //
  // This means that within this block, `myComponent` is of type `HTMLElement` and TypeScript
  // is happy for us to access its `innerHTML` property
  myComponent.innerHTML = 'updated content';
}

This is control flow analysis in action and it’s so useful when combined with non-nullable types.

Even though non-nullable types/“strictNullChecks” is an opt-in feature in TypeScript 2 and above, I would highly recommend that you do start all of your TypeScript applications with it enabled. You will catch more potential issues, and write higher quality code.

The TypeScript Team at Microsoft strongly encourages all users to enable strict null checks in their code, and as a result they made it enabled by default within tsconfig.json files which are generated via their helper tool:

tsc --init

Running that to start a new project is a great way to ensure that you have a solid foundation of the recommended compilerOptions for your code.

To summarize then, we’ve looked at the theory behind having null and undefined be their own distinct types in TypeScript 2 and above, setting “strictNullChecks”: true in our tsconfig.json which enables these non-nullable types, and how control flow analysis can utilise our if statements and similar such constructs to inform how the types flow through our programs.

We’ve also seen how the default type information for functions built into the DOM and other platform APIs get much stricter when we enable strict null checks, which ultimately gives us the best chance of shipping high quality code to our users.

Onwards!

James Henry's Picture

About James Henry

James is a passionate Senior Engineer who enjoys writing about development, software and open-source technologies.

Microsoft awarded James the title of Most Valuable Professional (MVP) for his contributions to the TypeScript project and its community.

A JavaScript expert and TypeScript evangelist, he regularly speaks at meetups and conferences around the world.

James is a member of the ESLint, Babel and Prettier teams.