Though given the popularity of default assignment, one could argue that most people are not in that camp :).
This conversation has caused me to reflect a lot on null
vs undefined
and if one way of treating it is better than another, and I've learned a lot about it as I've been trying to put this response together. I'll use TypeScript syntax to help me describe my thoughts. Also, when I say "a nullish-able value", I'm talking about a value that needs some sort of nothing state, e.g. "it's either a number, or " - the way to represent that nothing state depends on the philosophy, and the way I see it, there's two broader philosophies to look at here:
Philosophy 1. If you have a nullish-able value, then you can use either undefined
, null
, or both to represent its nothing state, it really doesn't matter. In TypeScript terms, its type would be TheNormalValue | undefined | null
. Just make sure that whenever you want to check if the value is in its nothing state, you use something like the == null
trick, which lets you check if its either undefined
or null
.
Philosophy 2. If you have a nullish-able value, then you need to consistently choose how to represent its nullish value. In TypeScript terms, you'd either use TheNormalValue | undefined
, or, TheNormalValue | null
for its type. Since the choice between the two is somewhat arbitrary, some people like to go a step further and say that you should always choose undefined
, not null
, to represent these nullish-able values. Others might choose null
for the absent of objects and undefined
otherwise, or they might try to follow that toilet-paper analogy, etc. Basically, I'm lumping all of those as branches under the same overarching philosophy.
I used philosophy 1 in the past but have since transitioned to philosophy 2 - more specifically, I use undefined
over null
for any public-facing APIs (so I'll often throw an error if you try to pass in a null
) - internally I'll sometimes use null
for a nullable type's nothing state, but honestly, it would probably make more sense if I just always used undefined
.
Here are some advantages to using this type of approach (philosophy 2):
- Allowing a public API to accept either
undefined
ornull
interchangeably (philosophy 1) increases the surface area of your public API, which in turn adds a little complexity to it, making it harder to fully test and maintain. In my case, if I were to accept eithernull
orundefined
, then I would probably write a test to verify that both of those work and behave exactly the same, and then a third test to verify that my function throws when given invalid inputs. Bus since I only acceptundefined
, notnull
, I can drop thenull
-specific tests, trusting that its behavior is being converted by my "invalid input" test. - There are a number of places where you can't treat
null
andundefined
as the same anyways. Some examples:- If your library provides a custom map implementation, the end-user would expect that if they put
null
into a map, they'll getnull
back out (and notundefined
). - If your library currently returns
null
, it would be a breaking change for it to suddenly start returningundefined
.
- If your library provides a custom map implementation, the end-user would expect that if they put
- Existing JavaScript (and TypeScript) syntax just works better with philosophy 2.
- You won't ever find yourself needing to reach for the ugly
== null
trick, because you always know what a given value's "nothing" state is represented as, so instead you can always explicitly use=== null
or=== undefined
. (And, of course, you can continue to use??
and?.
as well - those work fine withTheNormalValue | null
andTheNormalValue | undefined
types). - If you use TypeScript, it plays better with philosophy 2. TypeScript doesn't have any built-in syntax to represent
undefined | null
, which makes it more verbose to follow philosophy 1 while using TypeScript. - The point that's important for this discussion: If you follow the branch of always using
undefined
to handle your nothing states, then the current default parameter syntax will work perfectly - ifundefined
is given, it'll fall back to the default, and ifnull
is given, you can handle that the same way you handle other bad parameter types (such as throwing an error).
- You won't ever find yourself needing to reach for the ugly
So to address this point:
Yes, I think there's some harm, because:
- If we're arguing to provide better native support for philosophy 1, then there's a number of pieces of syntax that we'd have to add - a different default-assignment being just one of them. We'd also need to add a better alternative to the
== null
pattern, and TypeScript should really add a newnullish
type that representsundefined | null
- all of which is unnecessary baggage if you're instead using philosophy 2. - I do feel like philosophy 2 is objectively a better one to follow, and it doesn't really make sense to add native support for a code pattern that isn't as strong.
Also - just to be clear, it's not like I feel that philosophy 2 is leaps and bounds ahead of philosophy 1. It's more like philosophy 2 has a few, mostly minor nice things going for it, and I don't really see any advantages to using philosophy 1. So, by default, philosophy 2 wins.