null vs. undefined: Can I use only one?
Why do you wanna use only one in the first place?
null
and undefined
are used interchangeably to represent non-value. Why can’t I just stick with one?
At first, I used both because they have different definition
Here’s the definition from TC39:
- undefined — used when a variable has not been assigned a value
- null — represents the intentional absence of any object value
From a non-value perspective, I redefined them as:
- undefined — a non-value that you don’t explicitly assign.
- null — a non-value that you explicitly assign.
With above context, if I were to create an optional selectedDate
state that can have type of undefined
, Date
, and null
, each would mean:
- undefined — user haven’t selected date yet
- Date — user selected date
- null — user cleared selected date
In code:
In above example, selectedDate
having both type of undefined
and null
as non-value allows it to be distinguished whether it’s been touched by the user or not. You may want to allow submit button to activate only if user explicitly sets the selected date. It’ll be as simple as isDisabled={selectedDate === undefined}
.
But often times, there is no need to handle null
and undefined
separately. That means for above example, all I want to know is that whether there’s selected date or not:
Having both undefined
and null
are just adding more noise.
Then I started to wonder,
“Can I just use only one non-value?”
In fact, many other programming languages only have one non-value representation and they have no issue with it. Searching null
vs. undefined
in Google proves that having two non-values in Javascript is painful. It is even considered as design mistake by JavaScript’s creator, Brendan Eich.
So I wanted to simplify things by having only one non-value.
Using only null
The biggest downside of only using null
is that undefined
is everywhere.
Again, a variable is undefined
if it isn’t assigned to any value. And to prevent this, I would then have to assign null
on init:
let iDontKnowTheValueYet: null | string = null;
Using only null
gets more annoying in React w/ Typescript.
We often define props when creating component:
And when you want to pass an optional prop, a common way is to use ?
:
But this is a shorthand of optionProp: string | undefined
. So if we’re making it string | null
, then another problem occurs:
And there’re more but I believe above cases already justifies why sticking with null
is a bad idea.
Then… can I only use undefined?
Beforehand, there’s another question to answer:
Can I assign
undefined
to a variable in the first place?
It wasn’t safe to do so in the past but doesn’t matter in modern browsers.
In older browsers, it was possible to overwrite undefined
because it is not a reserved word unlike null
. So it leads to side effect if you do this:
But in modern browsers (JavaScript 1.8.5 / Firefox 4+), undefined
is a non-configurable, non-writable property, per the ECMAScript 5 specification.
So it’s no longer a problem if you’re using “modern browser”.
And there’re already some guys doing it: Typescript team only uses undefined and Douglas Crockford stopped using null.
So it’s worth a try.
But it didn’t last long
It’s because there are already too many APIs that produce null
for me to avoid. A lot of third party libraries required me to use null
.
Here, I’ll only discuss 2 essential libraries that forced me to use null
:
Firestore (of Firebase)
When querying documents by optional field
In Firestore, there’re two ways of representing optional field:
- Union of
null
2. No field at all
Since I was against null
, I went with #2.
But the issue with the latter is that you won’t be able to query documents “without” specific field (See the comment of Frank van Puffelen). While for null
, you can just do query.where(‘address’ == null)
.
So in above example, if you want to query all users with no address, you would have to query all users then filter them in client side. The huge downside with this approach is that Firestore charges by the number of documents you read. In short, you’ll pay more and write more code if you stick with no-null movement.
Updating an optional field
There’re two ways of updating document in Firestore:
- By using set operation
- By using update operation
While set
operation requires you to provide all document fields to update, update
operation only requires you to provide fields you want to update.
Using update
operation is convenient because you don’t always have to hold the whole document in order to update few fields.
But this brings confusion.
The confusing part occurs when undefined
is passed to update an optional field. Do you mean to clear the field or to preserve the existing value?
This question often occurs when designing API of a service that handles Firestore’s request and response. For the request payload, making all update-able fields optional makes sense for required fields, but ambiguous for optional fields:
As a solution, you can wrap with a generic type that determines whether to clear or preserve:
But this is ugly, isn’t it? How about just using null
to determine clear operation? If you decided to use null
to represent optional field in the previous problem, then using null
here would make more sense:
React
Using useRef
on element’s ref
property requires null
This becomes an issue if you’re using Typescript.
If you try to assign reference with value of undefined
to an element’s ref
, it throws a type error:
But if you pass null
as useRef
’s initial value, the error goes away.
This is because there’re two roles in useRef
:
- Role as a value container
- Role as an element reference
When you are using useRef
as a value container, you can change its value by modifying its .current
property. Therefore its type is MutableRefObject<T>
, and as its name states, you can mutate its .current
value.
When you are using useRef
as an element reference, you can only read values from it and React itself is the only one allowed to do writes. Therefore its type is RefObject<T>
and you cannot mutate its .current
value.
And the problem occurs when you create useRef
with an initial value of undefined
. If you create useRef
with undefined
, Typescript assumes that it’s a value container and returns type of MutableRefObject<T>
. This doesn’t match the type that element’s ref property is expecting and throws error.
Typescript returns RefObject<T>
only if useRef
is initialized with null
. To find out more why it is designed this way, read this and this.
So you cannot get away from null
if you’re using Typescript and has to use useRef
as an element reference.
You cannot return undefined in your React component
Although React doc says “false
, null
, undefined
, and true
are valid children. They simply don’t render.”, returning undefined
from a component throws following error:
Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
It clearly states that if want to render nothing, return null
. If not, you have to see that error. No workaround. Return null
.
But good news is that React 18 will allow rendering undefined
instead of throwing error. So starting React 18, you can opt to return undefined
when you want to return nothing.
Conclusion
Not using null
worked for Typescript team because none of their dependencies produced null
. And I guess Douglas Crockford may have to reconsider using null
again if he were to create a React app 😉.
But even with this experience, I still find it convenient to only use undefined
as non-value. It is simply because often times, I don’t need to use two non-values. Above exceptions rarely happen and are limited to number of libraries so I didn’t have to care much if I were to not add more libraries into my project.
So in conclusion, I follow this guideline:
- In general, use
undefined
overnull
to represent non-value. - Only use
null
if it can’t be avoided or brings more benefit than usingundefined
.
It was awkward at first because the guideline doesn’t strictly follow the definition of null
and undefined
, e.g. assigning undefined
to a variable. But once I get used to it, I find no meaning on being obsessed with the definition since it simply brings more benefit.
I would like to know if you had similar experience and how you ended up dealing with null
and undefined
😃
Thanks for reading!