undefined vs. null
Tags: typescript, javascript
Published: 2021-03-01 01:47:52

Have you ever wondered why JavaScript (and TypeScript) has two distinct keywords &mdash; `undefined` and `null` &mdash; that kind of feel similar? Most other languages have a `null` (or equivalent) keyword. It's usually a value you can assign to a variable, and it indicates an "empty" value, or a "sentinel" value. It works similarly in JavaScript too. But then why does JavaScript have another similar keyword called `undefined`? What's the difference between the two? How do we decide if we should use `undefined` or `null`? How does the usage of `undefined` or `null` impact types? This post will try to provide answers to these questions.

Let's get into it.

The main conceptual difference between variables with value `null` and variables with value `undefined` is the intention of the programmer. The rule of thumb I like to follow is **`undefined` is for unintentionally empty values, while `null` is for intentionally empty values.** This means that any variable which has not been assigned a value has the value `undefined`, whereas if a programmer wanted to assign an "empty" value to a variable, they would typically assign it `null`, which is interpreted as a sentinel value.

```javascript
let foo;
// `foo` is `undefined` because it's not set
console.log(foo); // undefined

let bar = null;
// `bar` is `null` because it is explicitly set to `null`
console.log(bar); // null

let obj = { baz: undefined };
// `obj['baz']` is `undefined` because it is explicitly set to `undefined`
console.log(obj['baz']); // undefined
// `obj['qux']` is `undefined` because it's not set
console.log(obj['qux']); // undefined
```

---

### Wait, this is too confusing...

Let's go through an example to see what this means.

Suppose we are writing some router logic, and want to add a `Cache-Control` header to the response if some flag `isCachingOn` was `true`.

We could do this:

```javascript
let headers = {};
if (isCachingOn) {
  headers['Cache-Control']: 'max-age=31536000'
}
res.set(headers);
```

But we could also rewrite this more succinctly as

```javascript
res.set({
  'Cache-Control': isCachingOn ? 'max-age=31536000' : undefined
});
```

Both these code snippets are equivalent. In both cases, if `isCachingOn` is `false`, we don't set the value of the `Cache-Control` property, which means we just end up passing `{}` as the argument to `res.set()`. Generally, setting something to `undefined` has the impact of "not setting" it (or "unsetting" it if already set).

But what if we set it to `null` instead?

```javascript
res.set({
  'Cache-Control': isCachingOn ? 'max-age=31536000' : null
});
```

Is this the same as the previous two snippets?

The answer is no.

In this case, if `isCaching` is `false`, we actually set the value of the `Cache-Control` property to `null`. This is not the same as "not setting" it. We are _explicitly_ setting it to a `null` value. We end up passing `{ 'Cache-Control': null }` to `res.set()`. Depending on what router framework we are using, this may very well work the same as the `undefined` example above, if the framework is smart enough to ignore headers whose value is set to `null`. However, from the perspective of the programming language, both are different, and this difference may be much more important in some cases.

---

### What about types though?

Okay, let's talk about types. What's the type of variables whose value is `undefined` or `null`? Do they even have a type? Can we assign `undefined` or `null` to a variable of `string` or `number` or `boolean` type?

In JavaScript, we can get the runtime type of a variable by using the `typeof` keyword. Depending on the actual value of the variable, we will receive one of the following strings back:
- `"string"`
- `"number"`
- `"boolean"`
- `"bigint"`
- `"symbol"`
- `"object"`
- `"function"`
- `"undefined"`

As you can see, `"undefined"` is one of the ECMAScript types. And as you can guess, we get this value when we try to use `typeof` with a variable whose value is `undefined` (i.e. it hasn't been set yet, or has been unset explicitly by setting it to `undefined`).

```javascript
let foo;
console.log(typeof foo); // "undefined"
let bar = 42;
bar = undefined;
console.log(typeof bar); // "undefined"
```

So far so good. But what about `null`? Why isn't there a `"null"` type. What happens if we use `typeof` with a variable whose value is `null`? The answer may surprise you.

```javascript
const foo = null;
console.log(typeof foo); // "object"
```

Wait, but doesn't type `"object"` imply that the variable is an object? Well yes, but actually no. As it turns out, in JavaScript, the type of `null` variables is also `"object"`. This is a bug since the early days of JavaScript, and unfortunately can't be fixed. You can read more about it [here](https://2ality.com/2013/10/typeof-null.html).

---

### Wait, isn't there a `null` type in TypeScript?

That's correct. TypeScript does have a `null` type. However, this is a compile-time type, used for compile-time type-checking. At runtime however, when the TypeScript code has been compiled to JavaScript, there is no `null` type. Instead, you only have the aforementioned runtime types.

Does this mean that we can't assign `null` to variables whose compile-time type is something else, like `string` or `boolean` or `number`? What if we have a function that accepts a parameter of type `string`, but we pass it an argument of type `null` instead?

```typescript
function debug(message: string) {
  if (message) {
    console.debug(message);
  }
}

debug("Testing debug log...");
debug(null); // Does this work?
```

The answer is, _it depends_. More specifically, it depends on whether or not we have strict mode turned on. Even more specifically, it depends on if we have set [`strictNullChecks`](https://www.typescriptlang.org/tsconfig#strictNullChecks) to `true` in our compiler options.

If we have `strictNullChecks` set to `false`, the TypeScript compiler will happily compile this code to JavaScript. But if we have `strictNullChecks` set to `true`, compilation will fail with the following error:

```
Argument of type 'null' is not assignable to parameter of type 'string'.
```

Note that if this compiler option is turned off, apart from `null`, it also allows us to assign `undefined` to variables of other types.

If we do have strict mode turned on like a good programmer, and we don't know if a variable can be `null` at runtime, we can use union types to allow the `null` type too.

```typescript
function debug(message: string | null) {
  if (message) {
    console.debug(message);
  }
}

debug("Testing debug log...");
debug(null); // No problem!
```

---

### How do I check if a variable is `undefined` or `null`?

This is a very common use case, and one where many programmers make mistakes. Often we want to add defensive checks inside functions which check if the argument passed is defined. Maybe we want to handle `null` arguments separately. How should we check if the argument is in fact `undefined` or `null`? 

In JavaScript, to check if a variable is `undefined`, we can do one of two things:

```javascript
let foo, bar;
if (foo === undefined) {
  console.log("foo is undefined"); // "foo is undefined"
}
if (typeof bar === "undefined") {
  console.log("bar is undefined"); // "bar is undefined"
}
```

We can either check if the value of the variable is `undefined`, or check that its type is `"undefined"`. These two are equivalent, because `undefined` is the only value which has a runtime type of `"undefined"`.

Unfortunately, this same tactic of checking the type will not work with `null`, because its type is `"object"`, which is not unique to `null`. So to check if a variable is `null`, we must explicitly check its value.

```javascript
let foo = null;
if (foo === null) {
  console.log("foo is null"); // "foo is null"
}
```

What about TypeScript though? TypeScript has a `null` type right?

Yes, but as mentioned above, it's only a compile-time type. In most cases, we don't even need to add such defensive checks in TypeScript, since the compile-time type checking does this for us if we have strict mode on. We won't be able to pass arguments of type `null` or `undefined` if the corresponding parameter type is something else.

However, there are cases when we aren't sure what type of argument we might receive (for example, if we are dealing with something that has been sent over the wire), and we do want to add runtime defensive checks. In that case, we can include the same checks as above, which will be included in the compiled JavaScript, and execute at runtime.