This article was originally published on Barbarian Meets Coding.
TypeScript is a modern and safer version of JavaScript that has taken the web development world by storm. It is a superset of JavaScript that adds in some additional features, syntactic sugar and static type analysis aimed at making you more productive and able to scale your JavaScript projects. This is the second part of a series of articles where we explore TypeScript's comprehensive type system and learn how you can take advantage of it to build very robust and maintainable web apps.
Haven't read the first part of this series? If you haven't you may want to take a look. There's lots of interesting and useful stuff in there.
JavaScript and the absence of value
null
is often referred to as The Billion Dollar Mistake. In the words of Tony Hoare who first introduced it in ALGOL in 1965:
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Tony Hoare and The Billion Dollar Mistake
Let's illustrate this pain with a simple example (although I'm pretty sure that you're likely familiar with it and have experienced it many times).
Imagine we have such a function that allows us to destroy our evil enemies:
// Destroy Thy Enemies!!
function attack(target) {
target.hp -= 10;
}
Hmm... It's Christmas. The time of joy, happiness and love. So let's switch the example for something more suitable:
// Love!!!!!
function hug(target) {
target.happiness += 1000;
}
Now THAT is much better! So imagine that we want to take advantage of the hug
function to spread some love around the world:
// Love!!!!!
function hug(target) {
target.happiness += 1000;
}
const sadJaime = {name: 'jaime', happiness: 0};
hug(sadJaime); // => Wihooo! ❤️
Great. Everything is going according to plan. We've made one person happier. Yippi! But what if we make our sample a little more convoluted?
Let's say that we want to go ahead and hug everyone. Like EVERYONE in the world. We have this magic Map that has been pre-populated (probably by Santa) with every single human being alive on the planet right now. So we verify that it does indeed work as marketed:
// Love!!!!!
function hug(target) {
target.happiness += 1000;
}
const sadJaime = magicMap.get("Jaime"); // that's me of course
hug(sadJaime); // => Wihooo! ❤️
Excellent! But what if the person we're trying to find doesn't exist or isn't alive on the planet right now?
// Love!!!!!
function hug(target) {
target.happiness += 1000;
}
// returns undefined because Eleanor is in The Good Place
const eleanor = magicMap.get("Eleanor Shellstrop");
// brace yourself
hug(eleanor); // => 💥💥💥 Much explosion.
// Cannot read property happiness of undefined
Yep. That's the problem right there. JavaScript just like Java, Ada and ALGOL suffers from the same ailment caused by the presence of null
. Although in the case of JavaScript, it may be worse because we have not one but two ways to represent the absence of value: null
and undefined
.
Both of these keywords represent the absence of value in JavaScript. Both will result in a null reference exception1 like the one described in the example above. So What is the difference between them?
There are several:
- As far as I know there's no native browser API that ever returns
null
. For the web platform, the absence of value is alwaysundefined
. - By convention, the community usually refers to
null
as the absence of value, whereasundefined
represents something that hasn't yet been defined. A developer could usenull
to denote a deliberate absence of value, whereasundefined
would be the web platform telling you that something hasn't been defined yet.null
may be also produced by APIs when interoperating between clients and servers (e.g. Java sending JSON to a JavaScript front-end). In fact, the JSON spec only supportsnull
and notundefined
. - Default arguments behave differently when given
undefined
ornull
. Pass anundefined
as an argument to your function and your function will use the default value that you provide. Pass anull
instead and you function will use it happily instead of your default value. (Which is mighty dangerous)
For all of the above, I try to steer away from null
as much as I can, and handle undefined
when it's produced by the platform. But even so, there will be times when you'll have no other choice that to work around null
and undefined
. And they will inevitably lead you to null reference exceptions and the very classic undefined is not a function...
So, How can TypeScript help us with this mess?
TypeScript and the absence of value
So TypeScript doesn't have two ways to represent the absence of value. It has four.
These four ways to represent the absence of value are:
-
undefined
andnull
just like in JavaScript since, after all, TypeScript is a superset of JavaScript -
void
which represent the return type of a function that doesn't return anything -
never
which represents the return type of a function that never returns (e.g. it throws an exception instead)
Wow! Four instead of Two?? Is this something good? And the answer is YES. Not because of the numerous ways to represent the absence of value but because in TypeScript null
and undefined
are also types.
So What?
Let's take a look at the previous example. We define the Target
interface to represent anything that has a happiness
property (which is all a human, pet or monster needs to be huggable):
interface Target {
happiness: number;
}
And now we can use it in our hug
function:
// Love!!!!!
function hug(target: Target) {
target.happiness += 1000;
}
If we try to hug sad Jaime as we did earlier everything shall be fine:
// Love!!!!!
function hug(target: Target) {
target.happiness += 1000;
}
const sadJaime = magicMap.get("Jaime"); // that's me of course
hug(sadJaime); // => Wihooo! ❤️
But what if we make the attempt to hug Eleanor?
// Love!!!!!
function hug(target: Target) {
target.happiness += 1000;
}
// returns undefined because Eleanor is in The Good Place
const eleanor = magicMap.get("Eleanor Shellstrop");
hug(eleanor); // => 💥 Argument of type 'undefined' is not assignable to parameter of type 'Target'.
We get an error!! We get a compile-time error. That is, as soon as we type the code above the TypeScript compiler will tell us that we're doing something wrong. Because the function hug
expects a Target
and we're giving it undefined
which is a completely different type that doesn't have any of the characteristics of Target
, the TypeScript compiler jumps in to help.
And so this is how TypeScript takes advantage of the null
and undefined
types to prevent you from running into null reference exceptions and the dreaded million dollar mistake. Awesome, right?
Strict Null Checking
The behavior we've just seen, where TypeScript will prevent you from shooting yourself in the foot with null or undefined is called strict null checking. It is a feature of the TypeScript compiler which was added in TypeScript 2.0 an which needs to be enabled in your tsconfig file via the stricNullChecks setting. In general, it is strongly advised to aspire to having your TypeScript configuration be as strict as possible. That way you'll take the most advantage of TypeScript's type system and you'll be able to write safer and more maintainable applications.
Just for kicks. How could we rewrite the function above to allow for null
or undefined
and be equivalent to the JavaScript version? One way to do it would be the following:
// Love!!!!!
function hug(target: Target | undefined | null) {
target.happiness += 1000;
}
So we're explicitly telling TypeScript that the argument of that function can be either Target
, undefined
or null
by using a type union. Or alternatively:
// Love!!!!!
function hug(target?: Target | null) {
target.happiness += 1000;
}
Where we use an optional argument using the target?
notation which is a shorthand for Target | undefined
.
Working with Null or Undefined
There'll be some situations in which you'll still need to work with null
or undefined
. Let's say that you're working with a third party library that is implemented in JavaScript and doesn't care much about returning null
or undefined
. You potentially have a mighty warrior that is about to use its sword to slash some potatos for the Christmas dinner, so you write this:
const warrior = tavern.hireWarrior({goldCoins: 2});
if (warrior !== null
&& warrior !== undefined
&& warrior.sword !== null
&& warrior.sword !== undefined){
warrior.sword.slash();
}
The warrior hiring API you were using was written in JavaScript and you can't be sure if it even returns a warrior, or if the warrior has a sword so to be on the safe side you are forced to write a bunch of null/undefined checks.
TypeScript (>=3.7) has an alternative and less wordy version to the pattern above. The optional chaining operator ?.
which lets us rewrite the above example:
warrior?.sword?.slash();
// The ? is often called the Elvis operator
// because this ?:-p
That is, we shall call the slash
method only when the warrior
and her sword
are not undefined nor null. That's a much nicer alternative if you ask me.
Yet another alternative to solving the issue with the hiring a warrior at a disreputable tavern is to have a default value at hand that we can use when things go awry. So in order to make sure that the slashing above takes place we could've followed the approach of always making sure that the mighty warrior does exist:
let warrior = tavern.hireWarrior({goldCoins: 2});
if (!warrior) {
warrior = new Warrior('Backup Plan Joe');
}
if (!warrior.sword) warrior.equip(new RustySword());
warrior.sword.slash();
TypeScript also has an alternative to the check-if-this-is-null-and-if-it-is-assign-this-other-value pattern: the nullish coallescing operator ??
.
Using ??
we can simplify the example above:
let warrior = tavern.hireWarrior({goldCoins: 2})
?? new Warrior('Backup plan Joe');
if (!warrior.sword) warrior.equip(new RustySword());
warrior.sword.slash();
Nice right? Both ?.
and ??
operators make it that much easier to work around the absence of value in TypeScript.
Optional chaining and the nullish coalescing operator were approved as part of ES2020 and are now official features of the JavaScript language. Yey!
Are There Better Ways to Model the Absence of Value?
A really cool way to model the absence of value comes from functional programming in the form of the Maybe
monad. In later articles of the series when we've dabbled in the mysteries of generic types we'll revisit this topic and learn about how TypeScript can support these functional programming patterns.
In Summary
null
and undefined
can cause havoc in your JavaScript programs. They can break things willy nilly at runtime, or they can force you into writing a lot of guard causes and defensive code that can be very verbose and obscure your core business logic.
In TypeScript null
and undefined
are also types. This in combination with strict null checks allows you to protect your programs from null reference exceptions at compile time. So that, as soon as you make a mistake that could have resulted in a bug sometime along the line at runtime, you get immediate feedback and can fix it right away.
When confronted with the need to work with null
or undefined
TypeScript 3.7 and ES2020 have two features that can lessen your woes. Optional chaining and the nullish coallescing operator will save you a lot of typing when dealing with null and undefined.
And that's all for today. Hope you've enjoyed the article and learned something new. Until next time, take care and have a wonderful day.
-
In JavaScript null reference exceptions are treated as the more general TypeError. There isn't a specific error that only applies to null reference exceptions like in other languages such as C# or Java (NullReferenceException). ↩
Top comments (4)
Nice writeup! Just a small nitpick: the optional chaining operator is actually
?.
, which you'll need to use when you're checking for a property with bracket notation, i.e.thing?.[property]
. If you writething?[property]
it'll try to evaluate it as a ternary and become sad.Thank you! And thanks for spotting that! 😄
I can
hug
you, sadJaime🤗