Skip to content
This repository has been archived by the owner on Jan 28, 2023. It is now read-only.

Strong evidence for treating null and undefined the same #20

Closed
ljharb opened this issue Jan 24, 2018 · 37 comments
Closed

Strong evidence for treating null and undefined the same #20

ljharb opened this issue Jan 24, 2018 · 37 comments

Comments

@ljharb
Copy link
Member

ljharb commented Jan 24, 2018

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?

@fatcerberus
Copy link

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);
}

@hax
Copy link
Member

hax commented Jan 29, 2018

@ljharb I think that issue only relate to iteration protocol and spread operator.

@ljharb
Copy link
Member Author

ljharb commented Jan 29, 2018

@Has the issue is indeed, but the research results are broadly applicable.

@hax
Copy link
Member

hax commented Jan 29, 2018

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) )

@ljharb
Copy link
Member Author

ljharb commented Jan 29, 2018

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.

@hax
Copy link
Member

hax commented Jan 29, 2018

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.

@claudepache
Copy link

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).

@hax
Copy link
Member

hax commented Jan 30, 2018

@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.

@claudepache
Copy link

@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.

@lostpebble
Copy link

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 is null because it has been deliberately set as null 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?

@Mouvedia
Copy link

Null values in the merge patch are given special meaning to indicate the removal of existing values in the target.

I could find many other real world examples; @lostpebble what matters are nullish coalescing common use cases.

@claudepache
Copy link

claudepache commented May 23, 2018

@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.

@ljharb
Copy link
Member Author

ljharb commented May 23, 2018

@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.

@lostpebble
Copy link

lostpebble commented May 23, 2018

@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?

@lostpebble
Copy link

lostpebble commented May 24, 2018

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.

@lostpebble
Copy link

lostpebble commented May 24, 2018

@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.

@ljharb
Copy link
Member Author

ljharb commented May 24, 2018

@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.

@lostpebble
Copy link

lostpebble commented May 24, 2018

@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.

@ljharb
Copy link
Member Author

ljharb commented May 24, 2018

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.

@lostpebble
Copy link

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.

@ljharb
Copy link
Member Author

ljharb commented May 25, 2018

That already exists via ES6 default arguments, which also work with destructuring. This operator, as proposed, is what’s needed most.

@claudepache
Copy link

claudepache commented May 25, 2018

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.

@hax
Copy link
Member

hax commented Jun 5, 2018

@lostpebble

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, undefineds 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.

@Mouvedia
Copy link

Mouvedia commented Jun 5, 2018

a ?? b for undefined only and a ?| b for both undefined and null

Interesting idea, but I am not seeing ?| being introduced without its pendant, ?&.
cf https://github.com/tc39/proposal-logical-assignment

@lehni
Copy link

lehni commented Jul 24, 2018

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.

@jeffvandyke
Copy link

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.

@Mouvedia
Copy link

Mouvedia commented Jul 24, 2018

An illustration of that is XMLHttpRequest: e.g. if onprogress is supported it's null else it's undefined.

@claudepache
Copy link

@Mouvedia

An illustration of that is XMLHttpRequest: e.g. if onprogress is supported it's null else it's undefined.

Sure. But how would you use concretely the nullish-coalescing (or the undefined-coalescing, or the null-but-not-undefined-coalescing) operator with onprogress?

@claudepache
Copy link

@jeffvandyke

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 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.

What would be the semantics of such an operator?

@Mouvedia
Copy link

Mouvedia commented Jul 24, 2018

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.

@claudepache
Copy link

@Mouvedia

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.

@jeffvandyke
Copy link

@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.

@Mouvedia
Copy link

Mouvedia commented Jul 24, 2018

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.

@cmawhorter
Copy link

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 "==="

@hax
Copy link
Member

hax commented May 9, 2019

@cmawhorter == and === is proved as a bad design by eslint eqeqeq rule. 🤪

@claudepache
Copy link

@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).

@ljharb
Copy link
Member Author

ljharb commented Jan 28, 2023

Closing, since this proposal is at stage 4.

@ljharb ljharb closed this as completed Jan 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants