Strong evidence for treating null and undefined the same
ljharb opened this issue · 37 comments
tc39/ecma262#1069 (comment) demonstrates, convincingly to me, that instead of “ES6 default arguments distinguish undefined and null” setting a precedent, that they in fact defy it - and that this proposal (and optional chaining) should thus stick with the established precedent, continued in most of ES6, that null and undefined should be treated the same.
Thoughts?
I would prefer that they not be treated the same. The behavior used for default parameters is ideal IMO. If I set a value to null
it’s because I want it to be null, and not be reinterpreted as any other value. If it’s undefined
then that means I didn’t set it and substituting a meaningful default is acceptable. In other words, I treat null
as a meaningful value in its own right.
JS is in a somewhat unique place having two distinct “null-like” values, and I wouldn’t want to lose the advantages of that by having the language unilaterally treating them as the same thing.
With regard to the connection to default parameters: I’ll just say that I would intuitively expect the following two bits of code to be equivalent:
function(a = 812) {
print(a);
}
Versus
function fn(a) {
a = a ?? 812;
print(a);
}
But it seems the table only cover the usage of iteration protocol (the constructor usages may be exception, but awb already explained that: tc39/ecma262#1069 (comment) )
Sure. But they’re still examples of many places where the language treats null and undefined the same, which is the relevance to this proposal.
Ok, we may need a much bigger table to collect all the behaviors...
As my understand, undefined
in the language is originally used for the cases which would be static error in static type languages (eg. missing argument, return void, out of index range, no exist prop access, etc.) Except those cases, I agree its semantic should be treat same as null
in most other cases.
I think that the traditional use of null/undefined is not a very important thing to consider in order to decide the semantics of ??
. One can find arguments one way or the other based on ideal grounds; but for practical purposes, I think it is more useful to be able to distinguish between nullish and non-nullish than between undefined and non-undefined. Consider:
Object.getOwnPropertyDescriptor(foo).set
— either a function or undefined;Object.getPrototypeOf(foo)
— either an object or null;- many DOM APIs return either an object of some type (e.g. a Node) or null.
In those cases you might or might not find a convincing reason why ”no value” is encoded precisely as undefined, respectively null; it doesn’t matter, because you don’t care when your goal is to provide a fallback.
Also, the two major cases I know where the distinction between undefined and null is important and indeed useful, namely JSON stringification and defaults, are situations where you provide a value, not where you get one. In those cases, it is in fact more useful to have ??
acting on both undefined and null, per #6 (comment).
@claudepache Unfortunately, your two examples are actually the cases where the distinction of null/undefined is important, because the reverse operations will throw TypeError if not use correct null/undefined values.
Object.defineProperty(foo, 'bar', {set: v})
throws TypeError if v
is null
.
Object.setPrototypeOf(foo, v)
throws TypeError if v
is undefined
.
@hax Yes, the distinction between null and undefined is sometimes important. My point is: it does not follow that it is useful to treat them differently when found at the LHS of the nullish-coalescing operator.
I disagree that there are not useful use cases for distinguishing between null and undefined. I think those differences in the JavaScript world are quite important.
It basically boils down to:
- if a value is
null
, it isnull
because it has been deliberately set asnull
at some point - if a value is
undefined
it means it is not a set value at all
Just that basic premise tells me that they are actually very different at a very base level.
In my personal use case, I use Google Cloud's Datastore as one of my data backends. Directly from their documentation (https://cloud.google.com/datastore/docs/concepts/queries#restrictions_on_queries):
Note: It is not possible to query for entities that are specifically lacking a given property. One alternative is to add the property with a null value, then filter for entities with null as the value of that property.
So this is what I do for properties which I still want to query on as being "void of value". I set them as null
.
But now if I want to use the ??
operator, it is pretty much useless for me in differentiating between something which has been deliberately set, and something which is actually undefined and unset.
Would it not be possible to perhaps have an extra, stricter operator for the cases where matching with undefined
/ completely unset values is needed?
I could find many other real world examples; @lostpebble what matters are nullish coalescing common use cases.
@lostpebble I agree that the difference between null and undefined is sometimes important, but it is not clear that the nullish-coalescing operator ought to be the specific tool to differentiate them.
A concrete example of code would help to understand the usefulness.
@lostpebble obj.prop = undefined
, there, now it's not null but it's still been explicitly set.
In other words, the convention you're talking about is merely that - a convention, and not a universal one.
@ljharb you may say it is "convention" but its made more universal by the fact that:
const x;
x === undefined // true
const y = {};
y.x === undefined // true
At the very core of JavaScript, when a variable is unassigned any value - it is now undefined
. This is what makes this more universal than convention.
One could argue deliberately defining a variable as undefined
is bad practice because it goes against this very basic way JavaScript makes use of undefined
. The option given to us for such scenarios is delete obj.prop
.
In any case, I would need to think further about where I may run into issues with this in the future. Potentially I won't, as @claudepache said earlier:
Also, the two major cases I know where the distinction between undefined and null is important and indeed useful, namely JSON stringification and defaults, are situations where you provide a value, not where you get one. In those cases, it is in fact more useful to have ?? acting on both undefined and null
It may be that my interactions with ??
never actually require me to know the difference between undefined
and null
. Only time and practice will tell, I just fear by then it's too late and this ship would have sailed.
@Mouvedia I've never run into that specification before - but it scares me. Guess I need to be extra careful with JSON stuff. Which surprises me because isn't null
a valid JSON value? How would one PATCH an update with a null
value?
So, I've been thinking a little bit - and for the sake of giving at least one example of where something like this could cause issues:
function takeValuesFromAnySource<T>(original: T, valueSource: any): T {
const newObject = {} as T;
for (const key in original) {
if (original.hasOwnProperty(key)) {
newObject[key] = valueSource[key] ?? original[key];
}
}
return newObject;
}
So basically any place where we'd want to actually return that null
value - which has been deliberately set as null
for very good reason - we instead get this resolving to the other value.
One solution would be to just continue using the current ternary operator:
newObject[key] = valueSource[key] === undefined ? original[key] : valueSource[key];
Not the worst thing. But it's an example at least of where this could cause issues.
Because of the dynamic nature of JavaScript, and as much as people dislike it, the difference between undefined
and null
will always remain an important distinction.
@ljharb I think the comment which you've pointed to in creating this topic kind of misses the point: tc39/ecma262#1069 (comment)
For any JavaScript operation which requires accessing of properties, null
and undefined
will act in exactly the same manner. This is not the issue at hand here. For example:
const values = {
something: null,
}
const valuesTwo = {};
let x = values.something.another; // TypeError
let y = valuesTwo.something.another; // TypeError
This is expected behaviour because (of course) we cannot access any property of null
or undefined
.
In all the examples shown on that comment, they are making use of null
and undefined
in manners where they would be accessed for their properties. It's pretty obvious that they would return similar if not exactly the same error messages.
It seems like you've used that comment to make a blanket statement that therefor the difference between the two is negligible - but it isn't really the best case at all to demonstrate that statement.
this proposal (and optional chaining) should thus stick with the established precedent, continued in most of ES6, that null and undefined should be treated the same.
This "precedent" that we cannot access properties on either null
or undefined
was never a precedent that needed setting in the first place. It was always there.
For Optional Chaining treating them the same actually still does make sense - because of the fact that accessing properties on null
and undefined
act in exactly the same manner and always have.
This operator ??
doesn't access properties - but instead evaluates the value of the LHS. In any place where we need to evaluate a value, the distinction (or at least the ability to distinguish) between null
and undefined
is important in JavaScript because of its dynamic nature.
@lostpebble that you can construct a code example where you wouldn’t be able to use this operator doesn’t change the overarching reality that the most common use cases require treating them the same. It’s fine to simply not use this operator if it doesn’t solve your problem.
The list given also gives many cases where evaluating the value treats null
and undefined
the same; that’s the precedent I’m referring to.
@ljharb I'm going through that list and I can't see any of the examples which involve evaluation. They mostly seem to treat the input value for x
as an iterator - which instead has been set to null
or undefined
and therefor throwing (the same) errors.
Could you please point out which ones evaluate so I can look deeper?
I'm well aware of the most common case, and not saying that we need to ignore having this operator evaluate for "nullish" LHS values (null
and undefined
both at the same time), I'm just making the case that as we have variable strictness in equality operators ==
and ===
for very good reason, and the fact that undefined
and null
differences exist in JavaScript for very good reason, there should be a way for this operator to differentiate as well.
It’s fine to simply not use this operator if it doesn’t solve your problem.
This seems dismissive. My position comes from a place of concern for potential future problems that people may unknowingly encounter - it's not just a personal preference. I will be well aware of whether or not I should use it.
I agree you might apply good reasons towards differentiating them in your code; but that’s a bit strong to claim that’s the reason for their original existence.
I apologize for coming off as dismissive; what i believe is that the thing that causes potential problems is differentiating null
and undefined
, and that it is much safer to treat them the same when possible.
I do get what you're saying @ljharb . And for most cases I completely agree that grouping null
and undefined
together makes life a bit easier.
It just worries me that there is a push in the JavaScript world to ignore the underlying reasons why these two separate states exist - and in turn trying to group them as one for the sake of simplicity. I believe there is still a place for them. Especially in equality operations - which is at the core of this specific ??
operator.
If you don't see it as important enough to include the use case for only matching undefined
then I will continue to work around it. Personally I would just love an additional, slightly stricter version of this for matching only undefined
- I feel like if JavaScript provides a new operator it should try cover all its unique quirks along with that.
That already exists via ES6 default arguments, which also work with destructuring. This operator, as proposed, is what’s needed most.
Because the nullish-coalescing operator may be thought, as the explainer of this repo says, as a mean ”to provide a default value”, some people may think hastily that it ought to be ”consistent” with defaults in function parameters and destructuring assignments. But use cases show something else. We design ”sugar” constructs in order to answer concrete needs, not ideological thoughts.
As it was first designed, defaults in function parameters and in destructuring had no special behaviour for undefined
: only a missing value triggered the default, an explicit undefined
did not. The intended behaviour was adjusted at some point, not because of some deep philosophical reason around the fine distinction between undefined and null, but in order to support common use cases related to forwarding arguments between function calls; see the meeting notes of July 2012 TC39 meeting for details.
Now, concerning nullish-coalescing, experience shows that an operator is needed, that makes the distinction between ”nullish” and ”value or object of some non-nullish type”. Here, deep philosophical reason around the similarity between undefined and null is not relevant, but the fact that treating null the same way as a non-nullish value is, in most cases, unwanted, and would even make the operator plainly useless.
It just worries me that there is a push in the JavaScript world to ignore the underlying reasons why these two separate states exist
As I see it, in no way the design choice for defaults on the one hand and for nullish-coalescing on the other hand constitutes a judgement about the value of the distinction between different nullishes in JavaScript. In some cases, that distinction is handy. In other cases, that distinction is unwanted, not because of its intrinsic irrelevance, but simply because it is not the information we are looking for in this situation.
I agree the differential of undefined
and null
is very important, and I used to consider whether we could introduce two operators, for example a ?? b
for undefined
only and a ?| b
for both undefined
and null
(and we already have a || b
for all falsy values).
But as my comment before, undefined
s are replacement for static errors, you are rarely use undefined
value directly, at least if you follow the best practice. (And if you use TypeScript/Flow, the compiler/typechecker will eliminate many unnecessary undefined
checks). So I think we do not need another undefined
-only coalescing operator, and instead of it we'd better write code clearer to indicate which undefined
really mean in each case. For example, use defaults in function parameters and in destructuring to indicate value absence.
Take your example:
function takeValuesFromAnySource<T>(original: T, valueSource: any): T {
const newObject = {} as T;
for (const key in original) {
if (original.hasOwnProperty(key)) {
newObject[key] = valueSource[key] ?? original[key];
}
}
return newObject;
}
I prefer newObject[key] = key in valueSource ? valueSource[key] : original[key]
which use in
for key existence check explicitly.
a ?? b
forundefined
only anda ?| b
for bothundefined
andnull
Interesting idea, but I am not seeing ?|
being introduced without its pendant, ?&
.
cf https://github.com/tc39/proposal-logical-assignment
I'd like to add my voice here with support for not treating null
and undefined
the same way. I think nullish coallescing is different from optional chaining in that when it comes to the validity of a value / object, they are to be treated the same way as required for optional chaining, but when it comes to the return of a defined value as opposed to an undefined value, null
is to be seen as a defined nothing, while undefined
is the seen as the undefined, default nothing, meaning nothing was returned / nothing was assigned.
An example:
const options = { setting: null }
const setting = options.setting ?? 'default value'
Explicitly providing null
doesn't mean the same as not setting it at all. There can be plenty of situations where this is a meaningful difference, and shouldn't be swallowed by the operator.
I like that this is being discussed, and I hope that what the right thing is becomes clear eventually.
On undefined vs null, the usual cases where I see undefined for is when I've made some kind of mistake, whereas null can be a valid default for unprovided arguments or properties (done by object destructuring, or from React, defaultProps). This is just my own usage and experience though.
I don't think the operator should swallow a difference others might care about, but it seems elegant to me to behave the same for both, but return null
or undefined
depending on the original value that went bad. I think this behavior would help users that want to be able distinguish between the two.
Perhaps also relevant, a brief check in Chrome's console shows that { ...null, a: 'a' }
and { ...undefined, a: 'a' }
both return the same valid non-erroring object.
An illustration of that is XMLHttpRequest
: e.g. if onprogress
is supported it's null
else it's undefined
.
An illustration of that is XMLHttpRequest: e.g. if onprogress is supported it's
null
else it'sundefined
.
Sure. But how would you use concretely the nullish-coalescing (or the undefined-coalescing, or the null-but-not-undefined-coalescing) operator with onprogress
?
On undefined vs null, the usual cases where I see undefined for is when I've made some kind of mistake, whereas null can be a valid default for unprovided arguments or properties (done by object destructuring, or from React, defaultProps). This is just my own usage and experience though.
It means that, in your usual cases, an operator that is based on the undefined/non-undefined dichotomy would be useless (except for debugging purposes), because you would not provide undefined but by mistake. (Or am I missing something?)
I don't think the operator should swallow a difference others might care about, but it seems elegant to me to behave the same for both, but return
null
orundefined
depending on the original value that went bad. I think this behavior would help users that want to be able distinguish between the two.
What would be the semantics of such an operator?
But how would you use concretely the nullish-coalescing (or the undefined-coalescing, or the null-but-not-undefined-coalescing) operator with
onprogress
?
Sadly you cannot because older versions of IE would throw if you add an unknown property to the instance. But that's irrelevant, what I wanted to illustrate is that specification implementors do use that convention to convey that something is supported but not set: it's not just us, users.
You didn’t understand my question, so I reformulate.
We do not need to be convinced that there is an effective and useful distinction between undefined and null.
We need use cases for deciding the semantics of the ??
operator.
So, very concretely: ignoring any old browsers bugs, could you write a snippet of code that uses both onprogress
and ??
, that takes advantage of the undefined-vs-null distinction, and that does something useful? If not, it is just useless to mention onprogress
.
@claudepache My usual cases, true, I wouldn't get much use out of undefined, but thinking about it more, there are some exceptions, such as objects that take different shapes, in a duck-typing style. I think I agree with those that say that this operator shouldn't be the place to differentiate between the two as far as its behavior goes.
Also, for semantics, I got my GitHub project emails mixed up - I was thinking of https://github.com/TC39/proposal-optional-chaining, which currently has this discussion (tc39/proposal-optional-chaining#65) going on, which seems rather relevant. Now I'm not sure what is right.
could you write a snippet of code that uses both onprogress and ??, that takes advantage of the undefined-vs-null distinction, and that does something useful?
Ill use IE9: it has a quirk which requires the XDomainRequest onprogress handler to be a function.
It's an anachronic example though: IE9 won't support ??
.
// req is an instance of xhr or xdr
// at that point opts.callbacks.onprogress is either
// - null
// - undefined (not reset to null so that we can deduce that it's not supported later on)
// - or the callback/function provided by the user of the library
req.onprogress = opts.callbacks.onprogress ?? function () {};
// somewhat equivalent to
if (opts.callbacks.onprogress != null)
req.onprogress = opts.callbacks.onprogress;
// but now IE might abort the request if a callback is not provided
If it's not clear, this example supports the current proposal.
reading these comments i've been convinced back and forth. lol.
even though i've long wanted ?? -- maybe "?" isn't the best character? what about:
||
as-is. anything falsy.|||
strict null/undef
there is precedent sorta with equals "==" and "==="
@cmawhorter ==
and ===
is proved as a bad design by eslint eqeqeq rule. 🤪
@cmawhorter even though i've long wanted ?? -- maybe "?" isn't the best character? what about:
||
as-is. anything falsy.|||
strict null/undef
I can’t answer better than #17 (comment).
Closing, since this proposal is at stage 4.