The purpose of *Null Tracking* in general (of which Nullable Types are only one of many different forms), is to somehow regain a modicum of safety (and sanity) in languages that have null references.
*If* you have the chance to eliminate null references altogether, that is a *much better solution* since the problems that null references cause *simply will not exist* in the first place. Sir Tony Hoare has famously said that he considers inventing the Null Reference his "Billion Dollar Mistake", which is actually a quite conservative estimate on the total costs that null references have caused until today. If even the person who invented them considers them a mistake, why would you willingly put them in a language?
C# has them because, well, they probably didn't know any better, and now they can't get rid of them because of backwards-compatibility. TypeScript has them because its semantics are based on ECMAScript's, which has them.
The *real* beauty of an `Option` type, though, is that it is isomorphic to a collection that can hold only one element. Dealing with collections is one of the most important parts of programming, and thus every language in the world has powerful collections libraries. And you can apply all of the work that has gone into collections also to `Option`s.
For example, if you want to execute an action with an option, you *don't* need to check whether it is defined! Every collection library on the planet has a way of iterating over a collection and executing an action for each element. Now, what does "executing an action for each element" mean for an `Option`? Well, if there is no element, then no action is executed. And if there is one element, then the action is executed once with that element.
In other words, `foreach` acts *exactly* like a `NULL` check! You can just blindly do
```scala
mightExistOrMightNot.foreach(println)
```
and it will print out the value contained in the `Option` if it exists and do nothing if it doesn't exist. The same applies when you want to perform a computation with the value. Every collections library on the planet has a way of iteration over a collection and transforming each element. Again, for an `Option` "transforming each element" translates to "transform the value or do nothing". So you can just do
```scala
val squared: Option[Int] = mightExistOrMightNot.map(_ ** 2)
```
Also, collections libraries have ways to flatten nested collections. Imagine you have a long chain of references, each of which could be `NULL`, and you wanted to access the last reference in that chain. With nested `Option`s, you just write
```scala
longListOfReferences.flatten
```
And if you want to get a value out of an `Option`, then you can simply write
```scala
mightExistOrMightNot.getOrElse(42)
```
and you will either get the value inside the option if it exists, or a default value of your choosing if it doesn't.
The *only* reason, really, for you to explicitly check for the existence of an `Option` is if you want to do something *completely different* in case the value is missing.
It turns out that `Option` is actually even more than "just" a collection. It is a *monad*. Languages like C#, Scala, and Haskell have built in syntax sugar for working with monads, and they have powerful libraries for working with monads. I will not go into details about what it means to be a monad, but e.g. one of the advantages is that there are some specific mathematical laws and properties associated with monads, and one can exploit those properties.
The fact that Java's `Optional` is not implemented as a monad, not even as a collection, is a significant design flaw, and I think is partially to blame for people not understanding the advantages of `Option`s, simply because some of those advantages cannot be realized with Java's `Optional`.
There is also a more philosophical reason for choosing an `Option` type over `NULL` references. We can call this "language democracy". There is a major difference between those two: `NULL` references are a *language feature* whereas `Option` is a *library type*.
*Everybody* can write a library type, but only the language designer can write a language feature. That means that if for my code, I need to handle the absence of values in a slightly different manner, I can write a `MyOption`. But I cannot write a `MYNULL` reference without changing the language semantics and thus the compiler (or, for a language like C, C++, Java, Go, ECMAScript, Python, Ruby, PHP with multiple implementations, *every single compiler and interpreter that exists, has existed, and will ever exist*).
The more the language designer moves out of the language into libraries, the more the programmers can tailor the language (really, the library) to their needs.
Also, the more the language designer moves out of the language into libraries, the more the compiler writers are forced to make library code fast. If a compiler writer figures out some clever trick to make `NULL` references fast, that doesn't help our hypothetical programmer who has written their own abstraction. But if a compiler writer figures out some clever trick to make `Option` fast, it is highly likely the same trick will also apply to `MyOption` (and `Try`, `Either`, `Result`, and possibly even every collection).
Take Scala, for example. Unfortunately, because it is designed to interoperate and integrate deeply with the host environment (the Java platform, the ECMAScript platform, there is also an abandoned CLI implementation), it has `null` references and exceptions. But, it also has the `Option` type which replaces the former and `Try` which replaces the latter. And `Try` first appeared in a library of helpers released by Twitter. It was only later added to the standard library. Such innovation is much harder to do with language features.
I can write my own Scala `Option` type, and I don't need to change the compiler for it:
```scala
sealed trait Option[+A] extends IterableOnce[A] {
def isEmpty: Boolean
override def knownSize: Int
def getOrElse[B >: A](default: => B): B
override def foreach[U](f: A => U): Unit
override def map[B](f: A => B): Option[B]
// … and so on
}
final case class Some[+A](value: A) extends Option[A] {
override val isEmpty = false
override val knownSize = 1
override def getOrElse[B >: A](default: => B) = value
override def foreach[U](f: A => U) = f(value)
override def map[B](f: A => B) = Some(f(value))
// … and so on
}
case object None extends Option[Nothing] {
override val isEmpty = true
override val knownSize = 0
override def getOrElse[B >: A](default: => B) = default
override def foreach[U](f: A => U) = ()
override def map[B](f: A => B) = None
// … and so on
}