There are many factors that contribute to null
being a "Billion Dollar Mistake". Based on your comments, you're looking specifically at TypeScript and the type of possibly-uninitialized values, so I'll boil it down to two key problems: type safety, and value semantics.
The key - a.k.a., tl;dr
Especially in TypeScript, null-ish values are perfectly valid so long as you assign a simple, clear meaning to it ("no value"), and so long as the language supports this by only permitting null-ish values when the type explicitly allows it.
TypeScript naturally guards against accidental null values.
Others have covered type safety, but the key here is that, in TypeScript, you can include or exclude null
and undefined
as valid values of a type. At least in strict mode, number
, number | null
, and number | undefined
are all separate types. Types are mathematical sets of valid values, and number
only covers numbers. That means that (barring any deliberate breaking of the type system), you don't have to worry about a value unexpectedly being null
or undefined
. The compiler won't let you accidentally do that. In that regard, the type safety of null-like values is not such a big deal in TypeScript, which just leaves semantics.
So what do null
and undefined
mean?
In short, they mean "no value" - nothing more, nothing less. The number
type can be any number, but it must be a number. In some cases, you want a type that's "a number, or nothing" - that's where you might use number | undefined
. If you want something other than "has a value or doesn't" - such as "technically a value, but invalid" or "there was an error trying to compute the value", you need to use something other than null
or undefined
. The exact solution depends on the exact use case - over-generalizing is what caused the problem in the first place!
So why are there two?
This illustrates the "semantics" problem of the Billion Dollar Mistake quite well! Your colleague was right to point out that, in JavaScript, undefined
means "was never defined", and shouldn't be allowed after initialization. That leaves null
to mean... anything else that isn't a real value? However, TypeScript is not JavaScript! Believe it or not, the team who creates TypeScript (which is, itself, written in TypeScript) does not use null
at all! These experts in the topics of language and especially of TypeScript and JavaScript have decided that null
does not provide any value above and beyond undefined
. But they do use undefined
.
I don't want to misrepresent them, but I believe that this is because they're focusing on what those values mean above and beyond how the JavaScript engines use them. Now, undefined
simply means "no value". They don't want to add any more than that, so they don't recognize a difference between that and null
.
What does this mean for my example?
There's some missing information about what null
is supposed to mean, but you seem to be doing the right thing. However, I would ditch the type Value = Node | null
and also switch to undefined
. Something like:
const [value, setValue] = React.useState<Node | undefined>()
function test(value: Node | undefined) {
if (value != undefined) // TS knows `v` is a `Node`, not `undefined`
else // TS knows `v` is `undefined`
}
Bonus: ?
operator vs explicit undefined
You can use ?
when declaring a value's identifier to suggest that it's optional. This automatically means it can be undefined
(as that is the default for when a value is not provided), but it means more than that. function test(value: Node | undefined)
means "test
requires one argument, which may be undefined", whereas test(value?: Node)
means "test
can be called with 0 arguments, or with a Node
, or with undefined
". It's a small difference, but sometimes you want to say "you must tell me what it is, even if you tell me there's no value", in which case you should avoid ?
.
null
is only a mistake if you don't know how to deal with it correctly. One way to deal with it correctly is to make sure all of your object references always have a value. Hence, your colleague's argument.