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

(null)?.b should evaluate to null, not undefined #69

Closed
claudepache opened this issue Jul 24, 2018 · 77 comments · May be fixed by #121
Closed

(null)?.b should evaluate to null, not undefined #69

claudepache opened this issue Jul 24, 2018 · 77 comments · May be fixed by #121

Comments

@claudepache
Copy link
Collaborator

claudepache commented Jul 24, 2018

In other words, the desugaring of a?.b should be a == null ? a : a.b (instead of a == null ? undefined : a.b as currently specced).

I gave the following justification for having a?.b === undefined when a === null in README#FAQ (emphasis added on the contended part):

Neither a.b nor a?.b is intended to preserve arbitrary information on the base object a, but only to give information about the property "b" of that object. If a property "b" is absent from a, this is reflected by a.b === undefined and a?.b === undefined.

In particular, the value null is considered to have no properties; therefore, (null)?.b is undefined.

However, that justification is flawed, because of the short-circuiting semantics: when a is null, the name of the eventual property (here, "b") is not sought, so that there is no property to give information about.

This is clearer when the property name is computed, as in, e.g.:

a?.[++i]
a?.[foo(a)] // where `foo(a)` throws a TypeError exception when `a` is null

In case of a?.[foo(a)], if a is null, we do not compute the value of foo(a) (and we couldn’t, if foo(null) throws a TypeError), so that it doesn’t make sense to try to ”give information about the property foo(a) of a”.


I think it is more sensible to apply the usual semantics of short-circuiting operators (&& and ||), at least for consistency reason, namely: In case of short-circuiting, evaluate to the last encountered value.

Incidentally, that would avoid to clear the null/undefined distinction of the base object, see #65.

@claudepache claudepache changed the title (null)?.b should evaluates to null, not undefined (null)?.b should evaluate to null, not undefined Jul 24, 2018
@0x24a537r9
Copy link

While a good edge-case to recognize, I think the counterpoint would be that you can actually give information about b in a.b if a is nullish, because you know for absolute certain that a.b does not exist, no matter the property name. Or to take a real world example, if you ask me "What color is the car?" (or ask me about any property of the car) when there's no car, I can confidently respond "That's undefined", and it would be the most direct answer to your question.

I could also respond "There is no car" (the equivalent of returning null, as I proposed in #65), but it assumes some intent behind the question (that questioning assumptions is more useful and just answering) rather than precisely answering it. Personally, I thought that would be the more useful answer most of the time (hence filing #65), but ultimately I recognize that it's less about which is more "correct" and more a subjective choice between which of those two valid behaviors ?. is intended to perform. At least it seems the community overwhelmingly expects the more direct answer ("The color is undefined") than the more intent-based answer ("There is no car").

@lehni
Copy link

lehni commented Jul 25, 2018

@0x24a537r9 adding to what you said, once the property is more than one level nested, e.g. in the question "what is the color of the car's seats", the answer null isn't clear anymore: It could be that there is no car, or the car is indeed there, but doesn't have any seats. That is why I believe undefined is the better answer here.

@0x24a537r9
Copy link

Eh, using undefined suffers the same issue. If you ask "What color are the car seats?" and receive undefined, it's equally unclear whether that's because there's no car or the car has no seats.

Both approaches lose information and can be ambiguous--it's just a choice of which cases you want to prioritize.

@hax
Copy link
Member

hax commented Jul 25, 2018

Agree with @lehni .

As we know, in many cases undefined in JS is playing the role of type checking errors in static type languages. So it's a good mappings that null.b throw error and null?.b returns undefined.

Further, most APIs may have type T | null (but rarely T | undefined) which null could convey special meaning. If a.b returns type T | null, a?.b should be T | null | undefined. Assume a?.b returns null, as current semantic, we would know it come from a.b and convey the special meaning of such API. But if we change the semantic that null?.x returns null, we will never tell whether it come from a.b or a. These two null may convey very different meaning.

@ljharb
Copy link
Member

ljharb commented Jul 25, 2018

I think if you need that certainty, you’d be unable to use this operator regardless, so that’s not an applicable argument.

@jrista
Copy link

jrista commented Jul 25, 2018

I think @hax nailed it. It is a matter of clarity and correctness. Shoving null through an optional chain is quite confusing. The simple fact of the matter is, null.b is undefined. It doesn't matter how we end up with a null there, doesn't matter how long the chain is, null is nothing and has no properties, so any attempt to optionally access a property on a null should result in undefined.

@rkirsling
Copy link
Member

I think it is more sensible to apply the usual semantics of short-circuiting operators (&& and ||), at least for consistency reason, namely: In case of short-circuiting, evaluate to the last encountered value.

I don't think this is an apt comparison—short-circuiting is based on a falsy check, not a nullish check. 🤔

Note that in o.get('a.b.c')-style stop-gap APIs for this feature, the result is uncontroversially undefined if we can't reach c. I know the syntax differs, but the intention is the same: it's about "reachability", i.e., either I get the value of c, or I fail to do so.

@Mouvedia
Copy link

Mouvedia commented Jul 25, 2018

We need another operator which returns the last property in the chain which is not undefined.

@ljharb
Copy link
Member

ljharb commented Jul 25, 2018

We definitely don't need another operator - also ?(a.b.c) won't work because that could be the positive branch in a ternary expression.

@0x24a537r9
Copy link

@rkirsling It is an apt comparison for ?? which is short-circuiting and based on a nullish check.

Also, the result of o.get('a.b.c') is not uncontroversially undefined. See idx().

That said, I think, unfortunately, both sides are talking past each other because they're not able to see that the other group is expecting fundamentally different behavior, and neither is inherently wrong--just different expectations. One group defines ?. to be "about" getting the property. The other group disagrees, defining ?. to be "about" both the property and the chain of access along the way (like a ?? a.b ?? a.c). One could conceivably have them be two separate operators (though that would be inadvisable due to confusion).

@rkirsling and @lehni, I'm not saying you're wrong, but as someone who was at one point on the other side of this interpretation (and now on the side of undefined), I can tell you that insisting that returning null is inconsistent with your definition of ?. is irrelevant to someone who disagrees with your definition. It is likely to be about as convincing as telling a Buddhist that they should act a certain way because the Bible says so--your fundamental expectations of what you are trying to do are different, so you can't apply the rules of one approach to another.

Those that believe that null?.a should return null may already understand that a is undefined under the expectation of how you are defining ?. but still persist in their beliefs because they fundamentally disagree with your premise that ?. should be just "about" a. They expect that ?. is closer to chained ?? operators, and that's a question of interpretation/expectation, not correctness. The issue at hand is which definition we should use for ?. (hence my survey in #65), and to advocate for one saying that the other doesn't cohere to the former's definition is circular--you can't say apples are better than oranges because oranges don't taste like apples.

@rkirsling
Copy link
Member

rkirsling commented Jul 25, 2018

@0x24a537r9

Also, the result of o.get('a.b.c') is not uncontroversially undefined. See idx().

D'oh, I guess that was wishful thinking then. (The specific cases I had in mind were Lodash and Ember.)

I think this operator is very desirable either way, so I'm sorry if my comment just added speed to wheels that are already spinning. Just hoped to provide a pointed statement of how one could conceptualize it.

@hax
Copy link
Member

hax commented Jul 26, 2018

Also, the result of o.get('a.b.c') is not uncontroversially undefined. See idx().

@0x24a537r9 I read #65 again, but still can not get why idx() choose the other way. Could you give some concrete use cases?

@0x24a537r9
Copy link

@hax As alluded to in the Usage section of the Github, it was intended to be a drop-in replacement for the existing pattern of a && a.b && a.b.c (while also being slightly, but usually inconsequentially, more rigorous by using nullish comparison rather than falsey). From that perspective, idx() and others like it are intended to return "the first non-nullish value in the chain", rather than "the possibly undefined value of the last item in the chain".

One advantage of this scheme is that if you ever receive undefined and the last property is required, you often have good signal that there's a bug and you're trying to look up a nonexistent field, not just running into an optional object along the way (null). In the proposed scheme for ?. you can't be sure whether you accidentally took an invalid codepath (and thus tried to look up an invalid field) or some object along the chain was simply missing as expected. Of course, there are tradeoffs on both sides.

@hax
Copy link
Member

hax commented Jul 26, 2018

@0x24a537r9 Still no concrete use case 🧐 ...

But I will try my best to understand your motivation according to your last comment. Please forgive me and correct me if I misread your comment.

One advantage of this scheme is that if you ever receive undefined and the last property is required, you often have good signal that there's a bug and you're trying to look up a nonexistent field, not just running into an optional object along the way (null).

Interesting idea, it seems you only want to deal with null but NOT undefined. The most interesting part is such idea is also based on "undefined is used to denote error" convention I mentioned before.

But the main advantage of ?. is ergonomic, so I can't imagine programmers will be happy to write extra guard code to differentiate undefined like:

const x = a?.b.c
if (x === undefined) {
   // something maybe wrong, but what can we do here?
}

And if you don't check the undefined, then you just delay the potential reference error to future calling/accessing, which make the bug much harder to find and debug.

The only possible "correct" solution I can imagine for this direction is:

Just throw Error instead of return undefined. Aka, make null?.x returns null and make undefined?.x throws (just like undefined.x throws). This is even much practical for TS/flow because they can avoid undefined?.x in first place (assume intentional usage of undefined is very uncommon in APIs).

The main problem of such semantic is: JS is not TS/Flow ... Especially for the guys who don't like static typings, they will very likely "abuse" ?. to eliminate all potential null/undefined errors. Aka, "good signal" is meaningless to them. So I'm afraid let undefined throws will not satisfy them.

In the proposed scheme for ?. you can't be sure whether you accidentally took an invalid codepath (and thus tried to look up an invalid field) or some object along the chain was simply missing as expected.

As my analysis before, null?.x returns null scheme can not practically solve the invalid codepath issue. So the counterargument is, if you really care about invalid codepath / field access, you should use TS/Flow anyway.

Of course, there are tradeoffs on both sides.

True. I believe the key point is if such tradeoff is practical.

@littledan
Copy link
Member

littledan commented Jul 27, 2018

This change matches my intuition, and I seem to not be the only one. I support it.

A lot of comments here and in other threads relate to what other libraries do. I believe there are several of these. Would anyone be interested in preparing a survey of the semantics of these libraries? It could be an interesting data point for comparison.

@claudepache
Copy link
Collaborator Author

claudepache commented Jul 27, 2018

The expectation of the “correct” result for (null)?.b highly depends on your mental model:

(A) a?.b is interpreted as ”the same as a.b, except that it does not throw for null/undefined”. For that interpretation, (null)?.b === undefined.

That interpretation is problematic in the face of short-circuiting (an objectively desirable feature) in expressions like a?.b.c. One could resolve it with the help of artefacts, such as using an adhoc Nil reference instead of a plain undefined value (that is how it was historically specced before #20).

(B) a?.b is interpreted as ”the same as a && a.b, except that we test for nullish-ity rather that falsy-ness”. For that interpretation, (null)?.b === null. (That interpretation matches more closely the current spec since #20.)

(After having written that comment, I realise that someone has already said that.)


But besides mental model, there are certainly some situations where it would be more useful (and sound) to have (null)?.b === null (concrete example in the next comment), and other situations where undefined is better. Given that undefined exhibits, more often than null, some special behaviour (either in builtin features like defaults, or in libraries), I think that it is safer to avoid producing undefined from null.

@claudepache
Copy link
Collaborator Author

As the majority seems to think that (null)?.b === undefined is better, here is a fictional example where it is not the case:

Consider the following function. At one point in time, black paint was the evident choice for cars:

function purchasePaintForCar() {
    // go to store and buy black paint
}

Later, more dyes became available, so the function was refactored:

function purchasePaintForCar(color = 'black') {
    // go to store and buy a paint of that colour
}

Now consider an object modelling a car, with a field color that contains the desired colour:

let color = myCar.color // "red"
purchasePaintForCar(color)  // buy red paint

(As an aside, that will work even for legacy car objects that doesn’t have the color field; so that purchasePaintForCar(myFordT.color /* undefined */) will purchase paint of correct dye. But that’s not my point.)

Now, there is a provision for using null in case the colour is not yet known, or even in case that, because of technological progress, there exist cars that do not need to be painted at all:

let color = myFutureCar.color // null
purchasePaintForCar(color)  // do nothing (don't buy paint)

Or even it is not certain that I will have a car in the future:

let myFutureCar = null 
let color = myFutureCar?.color // null
purchasePaintForCar(color)  // don't buy paint, even not black paint

@rkirsling
Copy link
Member

rkirsling commented Jul 27, 2018

@claudepache Now imagine that the car data is coming from a server, and we call one of the following:

purchasePaintForCar(response.data.defaultCar?.cosmeticOptions?.color);
// OR
purchasePaintForCar(response.data.cars[0]?.cosmeticOptions?.color);

In the empty case, defaultCar is most likely null, and cars is of course [].

Under your approach, these exhibit different behavior. Whether this is "correct" or not isn't too important (e.g., we could imagine a function updateCarUIColor(color = 'black') that doesn't expect null)—it's a question of least surprise. I would be hard-pressed to fault developers for forgetting that these won't be the same, as the distinction would be an unnecessary thought burden...until suddenly, it's not. Seems like a guaranteed "dammit!" moment to me. 😄

@ljharb
Copy link
Member

ljharb commented Jul 27, 2018

Why would defaultCar be null? In a server implementation I’d write, I’d probably omit the value entirely rather than inflate the bandwidth size just to match a strictly typed shape.

@rkirsling
Copy link
Member

rkirsling commented Jul 27, 2018

@ljharb Because it's a server you don't control, I guess. 😛
That response shape is inspired by a true story, but what I'm trying to convey is simply that imperfect JSON APIs are ubiquitous and wading through their data is the selling point of ?. for many people.

@jridgewell
Copy link
Member

I don't think either is clearly better than the other.

always undefined:

  • Pros
    • Lodash does this
  • Cons
    • It'll surprise some people
    • It can't be easily normalized to null

maybe null or undefined:

  • Pros
    • It can be easily normalized with ??: maybeNull?.prop ?? undefined
  • Cons
    • It'll surprise some people
    • Lodash doesn't do this

@ljharb
Copy link
Member

ljharb commented Jul 27, 2018

Given two choices where one makes the other impossible, and the other makes both possible, we should probably prefer the latter?

@jridgewell
Copy link
Member

we should probably prefer the latter?

Personally, I lean that way. But, _.get is entrenched with some 1,662,292 matches in public Github. There will be bugs.

@jhpratt
Copy link

jhpratt commented Jul 27, 2018 via email

@Mouvedia
Copy link

Mouvedia commented Jul 27, 2018

Is there a another language which has

  • a null/none/nil object
  • something analogous to a read-only undefined that you can assign (for example the del statement in Python wouldn't qualify)
  • the ? operator

?

@lehni
Copy link

lehni commented Jul 27, 2018

@jridgewell regarding normalisation:

In the first scenario, why can't it be normalized to null the same way as to undefined in the 2nd?

maybeNull?.prop ?? null

And regarding the 2nd scenario:

maybeNull?.prop ?? undefined

What if I'd like to distinguish the case where prop is null vs prop is not set (undefined)?

I think with such normalization, there is always a loss of information, in both cases. I don't see one being better necessarily than the other, just good for different kinds of situations.

@jridgewell
Copy link
Member

jridgewell commented Jul 27, 2018

Regarding the second, what if I want to know if a value exists, but is
null? Then we're needlessly complicating what should be a simple query.

I don't understand. You don't need to normalize.

In the first scenario, why can't it be normalized to null the same way as to undefined in the 2nd?

Because you can't tell if it was really null or undefined to begin with. So there's no way to "get the null out".

However, only cared if it was undefined (which is what the people arguing for (null)?.a === undefined are saying), you can ignore the null and normalize.

What if I'd like to distinguish the case where prop is null vs prop is not set (undefined)?

Then don't normalize?

@ljharb
Copy link
Member

ljharb commented Jul 27, 2018

_.get takes a string with dots in it too tho; I'm not sure it's something we need to align with.

@sndwow
Copy link

sndwow commented Sep 4, 2018

// this is ugly
const firstName = message?.body?.user?.firstName || 'default';

// maybe like this
const firstName = message.body.user.firstName ?? 'default';

@ljharb
Copy link
Member

ljharb commented Sep 4, 2018

@sndwow if you think it’s ugly, don’t write code like that (but i don’t agree). Those two things have very different semantics, and the second doesn’t allow you to, say, make body.user not optional.

@claudepache
Copy link
Collaborator Author

Here is a concrete use case where null?.foo() is expected to be null. I have a variable that contains either a string or null/undefined. I want to normalise the string:

let xn = x?.normalize("NFC")

When x is null/undefined, I expect to have xn === x.

@ljharb
Copy link
Member

ljharb commented Dec 1, 2018

@claudepache can you elaborate more on why if x is null, you’d need xn to be null instead of undefined?

@hax
Copy link
Member

hax commented Dec 1, 2018

I think the essential issue is what information we can get from the result, especially how differentiating null/undefined could be useful.

  • a.b throw TypeError means a is nullish
  • a.b === undefined means a is not nullish and do not have property b
  • a.b === null means a is not nullish and have property b with value null

If use TS/flow, compiler only allow a.b in the branch of a is not nullish, and eliminate the "do not have property" case, so

  • a.b === null means a is not nullish and have property b with value null
  • instead of throw, have to write if (a == null) { ... } for a is nullish
  • rarely use a.b === undefined (only useful in edge case: a have an optional property b in type T | null)

Basically, type system eliminate most foo === undefined, with the cost of writing if (foo != null).

Now let's introduce optional chaining operator, use a?.b instead of a.b

As current semantic,

  • a?.b throw TypeError means a is nullish
  • a?.b === undefined means a is nullish, or a is not nullish and do not have property b
  • a?.b === null means a is not nullish and have property b with value null

We can see current semantic just combines the first two cases and keep last unchanged.

If use TS/flow, it becomes

  • a?.b === undefined means a is nullish (so do not need write if (a == null))
  • a?.b === null means a is not nullish and have property b with value null (same as a.b === null mean)

As null?.foo === null semantic

  • a?.b throw TypeError means a is nullish
  • a?.b === undefined means a is undefined, or a is not nullish and do not have property b
  • a?.b === null means a is null, or a is not nullish and have property b with value null

We can see the first case are splitted and mixed into other two cases.

If use TS/flow, it becomes

  • a?.b === undefined is not very useful
  • a?.b === null means a is null, or a.b is null (a is not nullish and have property b with value null)

Summary

Current semantic make a?.b === null always have the same information as a.b === null, make a?.b === undefined have the same information as a == null || a.b === undefined. In ts/flow, a?.b === undefined have the mostly same information as a == null.

null?.foo === null semantic make a?.b === null have the same information of a === null || a.b === null, make a?.b === undefined have the same information of a === undefined || a.b === undefined. In ts/flow, a?.b === undefined is not very useful.

I think TS/flow users would definitely prefer current semantic, considering a?.b === undefined is not useful in null?.foo === null semantic.

Even only consider JS, in long chaining like a?.b?.c?.d,

As null?.foo === null semantic,

  • a?.b?.c?.d === undefined give a === undefined || a.b === undefined || a.b.c === undefined || a.b.c.d === undefined information
  • a?.b?.c?.d === null give a === null || a.b === null || a.b.c === null || a.b.c.d === null information

As current semantic,

  • a?.b?.c?.d === undefined give a == null || a.b == null || a.b.c == null || a.b.c.d === undefined information.
  • a?.b?.c?.d === null give a.b.c.d === null information

Could null?.foo === null have more use cases than current? I very doubt.

@hax
Copy link
Member

hax commented Dec 1, 2018

And, I think current semantic is more consistent with semantic of nullish coalescing operator.

Actually I would like this proposal rename to "non-nullish chaining" or "nullish stop chaining" 😉

@Mouvedia
Copy link

Mouvedia commented Dec 1, 2018

Isn't default the nail in the coffin for null?

I mean who wants to have to write foo(a?.b ?? undefined)?

@claudepache
Copy link
Collaborator Author

@ljharb

@claudepache can you elaborate more on why if x is null, you’d need xn to be null instead of undefined?

I don’t need it to be specifically either null or undefined. But given that undefined and null have sometimes different semantics (as a fact of life, not judging whether it is appropriate), it may be dangerous to change null into undefined or vice versa in an otherwise unrelated operation (here, unicode normalisation). Compare:

foo(x);
// carelessly refactored as:
foo(x?.normalize("NFC"));

(Even if we agree that it would be generally weird for foo() to consider null as a reasonable input and to have a default parameter value different from null...)

@ljharb
Copy link
Member

ljharb commented Dec 1, 2018

Thanks for explaining, ftr it makes the most sense to me that optional chaining shouldn’t change the LHS.

@claudepache
Copy link
Collaborator Author

@hax We can discuss at length what semantics is more appropriate in theory, and I don’t think we can find an answer that is always correct, even if there are chances that one of the semantics is more often correct. (But do note that I gave an example of a?.b(), not a?.b.) However:

I think the essential issue is what information we can get from the result, especially how differentiating null/undefined could be useful.

I am less worried about what information I get from a?.b (or a?.b(), etc.), for if I want to make the fine distinction between null and undefined based on information about a, I don’t mind to write one or two more lines of code in order to explicit my intent. (But I expect on contrary to add sometimes either ?? null or ?? undefined, whatever the outcome of this issue.) — I am more worried about information I accidentally change (as in my example).

@rkirsling
Copy link
Member

rkirsling commented Dec 1, 2018

@ljharb

optional chaining shouldn’t change the LHS

Unfortunately though, this phrasing already presupposes the "null passes through" viewpoint. The "always undefined"* camp would never describe this as "changing the left operand", because from this viewpoint, a?.b is purely "a query for b", and the value of a was never asked for in the first place.


* I actually fear that the phrase "always undefined" itself bakes in this presupposition; it may be better to call the two viewpoints "querying (for b)" and "preserving (a)" or similar. This would hopefully better reflect the fact that each camp is working from different axioms and that ultimately the committee will have to choose one or the other.

@ljharb
Copy link
Member

ljharb commented Dec 2, 2018

I guess i see a?.b as sugar for a == null ? a : a.b, and not a == null ? undefined : a.b or (a == null ? {} : a).b. Is there a different desugaring you have in mind that might help convince me?

@rkirsling
Copy link
Member

I think I tend to view the specific desugaring as secondary, i.e., as a representation of the proposed semantics which must then be verified, as opposed to an expression of the fundamental motivation.

In particular, there's no expectation that the most convenient way to write something with ?. will be semantically equivalent to the most convenient way without it (after all, short-circuiting with && isn't completely safe, but one grins and bears it when considering the verbosity of the alternative).

Thus we could have a before-and-after scenario like this:

// before
function getFooStatus(options) {
  return options && options.fooData ? options.fooData.status : defaultStatus;
}

// after
function getFooStatus(options) {
  return options?.fooData?.status ?? defaultStatus;
}

These clearly aren't equivalent under any desugaring, but each might be called "the easy way" given the features available.

So while the desugarings you've mentioned represent the proposed semantics of each camp, I'm encouraging that we recognize that these representations merely follow from each camp's axioms—are we just querying for b or are we hoping that any non-undefined data would pass through from a?

@Zarel
Copy link

Zarel commented Dec 4, 2018

On the other hand, a.b !== undefined is my personal pattern for "does a.b exist?" – since I always use null and not undefined for nil values, !== undefined becomes an existence check. I would intuitively expect a?.b !== undefined to work the same way.

If a?.b falls back to a, then suddenly the result is ambiguous no matter what.

It's also nice if a?.b could be typed as T | undefined. T | undefined | null is a significantly less wieldy type, especially since it turns a?.b !== undefined into either a?.b != undefined (which is banned by default in many linters) or ![undefined, null].includes(a?.b) which is pretty ugly.

@hax
Copy link
Member

hax commented Dec 4, 2018

@claudepache

foo(x);
// carelessly refactored as:
foo(x?.normalize("NFC"));

This is an interesting example. If I understand this example correctly, we are assuming foo(x) have different semantics for x being undefined or null, and in this code, x is null, so it's foo(null) before refactoring, and become foo(undefined) after refactoring as current semantic.

But similar cases also occur in other side:

  const {x} = bar || {};
  foo(x);
  // refactored to:
  foo(bar?.x);

Assume bar is null, using null?.foo === null semantic, refactoring will cause foo(undefined) change to foo(null).

@lucasbasquerotto
Copy link

If taking into account the JSON representation of javascript objects, I find null?.b === undefined as the best option, because, considering objects like:

1) o1 = { a: null }

and

2) o2 = { a: { b: null } }

I think of undefined as a value that was not defined in the object at all, while null as something explicitly defined with null, so in the first case o1?.a?.b === undefined (because there isn't a property a.b in o1) and in the second case o2?.a?.b === null (because there is a property a.b in o2 with the value null).

Also, o1?.a === null and o1?.c === undefined.

If o3 === null and o4 === undefined, then:

o3?.a === o4?.a === undefined
o3?.a?.b === o4?.a?.b === undefined
o3?.a?.b?.c === o4?.a?.b?.c === undefined
and so on...

That said, I would be okay with any approach, just want that this feature (optional chaining) be included in javascript.

@ghermeto
Copy link
Member

I think of undefined as a value that was not defined in the object at all, while null as something explicitly defined with null, so in the first case o1?.a?.b === undefined (because there isn't a property a.b in o1) and in the second case o2?.a?.b === null (because there is a property a.b in o2 with the value null).

I agree

@obedm503
Copy link

@lucasbasquerotto

I think of undefined as a value that was not defined in the object at all, while null as something explicitly defined with null

well, it depends on how you check.

const obj = { a: null, b: undefined };

Boolean(obj.a); // false
Boolean(obj.b); // false
Boolean(obj.c); // false

// property actually exists, but null or undefined
'a' in obj; // true
'b' in obj; // true
// property does not exist
'c' in obj; // false

// ignores undefined's
JSON.stringify(obj); // {"a":null}

// the properties really are in the object
Object.keys(obj); // ["a", "b"]

just providing more context/info to how js works. not expressing an opinion.

@ghermeto
Copy link
Member

@obedm503 I don't think that was the point.

var a = {};
console.log(a.b); // undefined

@lucasbasquerotto
Copy link

lucasbasquerotto commented Jun 12, 2019

@obedm503 I'm well aware that a key with an undefined value and a key that don't exists are not strictly the same in javascript, but:

1) A property defined with undefined and a non-existent property have the same value (in your example, obj.b === obj.c (both undefined) , but obj.a !== obj.c).

2) Like I said in that post: If taking into account the JSON representation of javascript objects[...], I was considering how an undefined property is passed around when converted to JSON (or, said in another way, how an object that contains it is serialized). In this case it doesn't make a difference between obj.b (declared as undefined) and obj.c (not declared).

3) It's common to see a variable defined as undefined as a syntactic sugar / language feature for properties defined dynamically, like var obj = { a: myVar }, where myVar comes from another place and may be undefined (and thus allowing complex objects having undefined properties, even tough the property is "defined"). So, in this situation, you don't care about the difference between an explicity or implicit undefined, only the value, that is the same in both cases.

4) This issue is mainly between null vs undefined, and I consider undefined as the best option, independently of considering a property explicitly defined with the undefined value, or not defined at all, because in both cases the value of the said property is undefined (and that is why I think (null)?.b should evaluate to undefined). See what @ghermeto said above.

What you said is valid and it's important to keep in mind, but I don't think it conflicts with what I said in that post and the reasons to choose null over undefined.

claudepache added a commit that referenced this issue Aug 21, 2019
As there is no canonical semantics, links to the relevant discussion threads for the various points of views. Closes #69. Closes #65.
@caub
Copy link

caub commented Aug 27, 2019

It's quite surprising that null?.a.b evaluates to undefined, while (null?.a).b throws

But since
null?.a.b is null == null ? undefined : null.a.b and (null?.a).b is (null == null ? undefined : null.a).b it makes sense

@claudepache
Copy link
Collaborator Author

It's quite surprising that null?.a.b evaluates to undefined, while (null?.a).b throws

Why on earth would you write (null?.a).b?

@caub
Copy link

caub commented Aug 27, 2019

I would not write this intentionally for sure, but it's valid syntax, and static property accessor is a left-to-right operator, so I could expect a same result

But I'm fine with the current status, it's just maybe something to be aware of

@caub caub mentioned this issue Aug 27, 2019
@claudepache
Copy link
Collaborator Author

Now that the proposal is at stage 4, that won’t change.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.