Nullish coalescing syntax for default param values

Would there be any support for the idea of accepting nullish-colescing type of syntax for the default parameter values?

The syntax would use the provided default value for both null and undefined param values, just like nullish

In this example:

function setColor(color ?? '#aaa') { }

would be similar to:

function setColor(color = '#aaa') { }

except it would not only handle color equal to undefined but also null, just like : const foo = null ?? 'default string'

Of course the syntax would apply to each parameter in case of multiple parameter functions.

Supporting this would result in much simpler code, where you need to guard your code against null values. An example could be Angular where resetting form controls results in their value being null rather than undefined. Should you want to process the values for other purpose, you need to capture the nulls properly.

Also, the proposal seems like a very legit counterpart of the already supported object nullish coalescing in assignments - all in all, a default parameter is also an assignment.

I think the current

function setColor(color) {
  color ??= '#aaa';
  …
}

is better to read than cramming all the logic in the default initialisers. Also I prefer the concept of parameter defaults applying only when you pass no value (to which passing undefined is equivalent), instead of supporting guards to deal with nullish, falsy, non-object or non-iterable values.

I understand where you are coming from, but personally I don't see defaults as equivalent to assignments.

2 Likes

There is an on-going issue on the proposal ^

I would also vote to not change the built-in semantics of default arguments.

2 Likes

Should this be revisited? This was on my list of potential proposals and I am disappointed to see this was suggested but ultimately ignored.

The fact that JavaScript has 2 different types to represent Nothing is a source of a lot of confusion and results in overly verbose code to handle both values. Default parameters is a good example of this. The fact that default parameters do not apply to null values is unintuitive. For example, the following is a common mistake:

function example(foo: string | null = "hi") {
  console.log(foo);
}

example(null); // undefined

The nullish-coalescing proposal has since been finalized. I would like to propose adding the nullish assignment syntax to function parameters. For example, the previous example could be written as this to account for both null and undefined:

function example(foo: string | null ??= "hi") {
  // The type of foo is narrowed to string
  console.log(foo);
}

example(null); // "hi"

What do you think?

I'm going to take a few steps back. Perhaps, as a community, we've got to figure out what the difference is between null and undefined and how they should be used. I've seen lots of different philosophies, including:

  • Pretend null doesn't exist, only use undefined.
  • Make undefined and null mean the same thing everywhere you can
  • Use null in the absence of an object and undefined in the absence of a primitive
  • I saw once an analogy of "null" being like an empty toilet paper roll and "undefined" being like the role doesn't even exist. Sounds nice in theory, but I honestly have no idea how to apply this concept in any real-world scenario. But, I guess the point here is that some people like trying to apply semantic differences between the two.
  • It's not uncommon to see APIs that take an object as input and will update a given resource using that object. If a property is set to null, then it will be deleted, and if a property is omitted or set to undefined, then the existing value will be preserved (that property won't be touched).

I bring this up, because the way we treat undefined and null, and the way we expect others to treat it, effect how we expect default parameters to work. If we're in the camp where we expect undefined and null to be treated the same everywhere, then yes, JavaScript's behavior for default parameters is confusing. If we're in one of those camps that assign different meaning to null and undefined, then perhaps it's less weird to encounter a function that uses undefined as a fallback and null as something else.

1 Like

The value null is intended to represent “no object” in contexts where an object value is expected. It is modeled after Java’s null value and it facilitates integration of JavaScript with objects implemented using Java.

https://www.wirfs-brock.com/allen/jshopl.pdf#page13

So null represents “no object” and is an unfortunate consequence of Sun Microsystems' influence over the final design of JavaScript (the original prototype did not have null).

Regardless of someone's "philosophy" of what null "means", is there really any harm in adding a syntax that allows developers to treat them similarly if that is their preference? Given the popularity of nullish coalescing, I would argue most people are in the camp of treating them as the same semantically.

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.

I don't think that is true. I think most people are surprised when they discover that default values do not apply to null arguments.

There is a 3rd philosophy that you have conveniently left out. That is the philosophy that you should avoid undefined and always use null. While not my philosophy, and admittedly likely smaller than the community around the other 2 philosophies, it is a valid philosophy that some people have.

One reason someone may have this philosophy is because undefined is not valid JSON but null is. And indeed, when working with JSON APIs, one typically needs to confront null.

However, there are actually an infinite number of philosophies one could have. For example, another philosophy may be to avoid both null and undefined and use a new data structure or symbol to represent the absence of a value.

In my opinion, this philosophy argument is not an argument against adding nullish coalescing default parameters.

Also, while we are both using TypeScript in our examples, keep in mind that JavaScript does not have static typing and there are folks that still use vanilla JavaScript without the support of a type system.

I consider it a mistake that parameter defaults only apply to undefined and not also to null, personally, and I’m glad optional chaining and nullish coalescing didn’t repeat that mistake.

2 Likes