Undefined in TypeScript
Every programming language has the concept of “no value”. Many follow the
example of C and its NULL
; Ruby has nil
, Python None
. More recent and
functional languages like Rust or Elm use a union type, like Option<T>
or
Maybe a
, with a union member representing “nothing”, but lets ignore those for
now.
JavaScript is different in that it has two primitive types to represent the
absence of a value,
null
and
undefined
.
I’m personally not aware of another language that does this.
Undefined
Most of us programmers have written at least some JavaScript by now, and
probably first encounter undefined
with the infamous “undefined is not a
function”. This happens because JavaScript runtimes use undefined
as the
“uninitialized” value; either a variable without an assignment, or a
non-existent field of an object. The latter often being a typo or type-mismatch.
undefined
is built into the semantics of the language. A bare return
statement with no value causes a function to return undefined
. A function with
no return
at all also returns undefined
. A function with an optional
parameter has its value initialized as undefined
when no argument is given.
Null
On the other hand, null
does not “naturally” occur through the usage of
language constructs. A programmer needs to explicitly initialize, pass, or
return a value of null
. It’s much more intentional, and meant to represent the
explicit absence of data.
This is in contrast to undefined
, which is often more like “Oops, you are
trying to access something that doesn’t exist” by mistake.
Inconsistency
Consider methods meant to retrieve data. Your “fetchers” and “getters” of the
world. From my description of the undefined
and null
, you might expect that
these all prefer null
. Here are some common browser APIs:
API | null |
undefined |
---|---|---|
Array.prototype.find() |
✓ | |
Cache.match() |
✓ | |
Document.querySelector() |
✓ | |
FormData.get() |
✓ | |
Headers.get() |
✓ | |
Map.get() |
✓ | |
Storage.getItem() |
✓ | |
URLSearchParams.get() |
✓ |
Yeah, they aren’t consistent at all. My random sampling leans towards null
,
but barely so. Maybe Map.get()
is trying to be consistent with []
access of
an object when the key doesn’t exist, but then why does FormData
return null
for the same case?
TypeScript
When you start writing TypeScript, you run headlong into this inconsistency. If
you don’t give it much thought, you end up with long union types everywhere like
string | null | undefined
as the results of various operations get smashed
together.
You might react to this with a “pick one” policy, with most folks picking
undefined
, since it shows up naturally from basic language semantics. Just
don’t use null
whenever possible. In some ways, this mirrors the other
languages like Ruby, with undefined
playing the role of nil
.
However, I’ve seen this approach backfire. I’ve used a popular ORM where a field
with a value of undefined
meant “don’t update this field in the resulting
SQL”, while null
meant “set the underlying column value to SQL NULL
”. This
lead to subtle bugs where data was not being cleared, since the rest of the
codebase was trying to prefer undefined
.
Why Not Both?
Ruby is a language I love and so in general I enjoy the simplicity of a single
null-like value, nil
. But surprisingly there are cases where using nil
ends
up being lossy.
If you define a method that takes an optional parameter, and the caller passes
no value, you’ll generally assign it the default of nil
:
def foo(a, b, options = nil)
# Do your foo thing
end
But what if the caller explicitly passes the optional parameter as nil
? It
may not matter for your particular method, but certainly sometimes it does. See
how Rails
uses object singletons to detect this.
So maybe the concept of undefined
has its place, and we should use it
alongside null
judiciously.