Nullish coalescing syntax for default param values

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 or null 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 either null or undefined, 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 accept undefined, not null, I can drop the null-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 and undefined 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 get null back out (and not undefined).
    • If your library currently returns null, it would be a breaking change for it to suddenly start returning undefined.
  • 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 with TheNormalValue | null and TheNormalValue | 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 - if undefined is given, it'll fall back to the default, and if null is given, you can handle that the same way you handle other bad parameter types (such as throwing an error).

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 new nullish type that represents undefined | 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.