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
undefinedornullinterchangeably (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 eithernullorundefined, 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
nullandundefinedas the same anyways. Some examples:- If your library provides a custom map implementation, the end-user would expect that if they put
nullinto a map, they'll getnullback 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
== nulltrick, because you always know what a given value's "nothing" state is represented as, so instead you can always explicitly use=== nullor=== undefined. (And, of course, you can continue to use??and?.as well - those work fine withTheNormalValue | nullandTheNormalValue | undefinedtypes). - 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
undefinedto handle your nothing states, then the current default parameter syntax will work perfectly - ifundefinedis given, it'll fall back to the default, and ifnullis 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
== nullpattern, and TypeScript should really add a newnullishtype 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.