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

Changing null -> undefined potentially dangerous #65

Closed
0x24a537r9 opened this issue May 30, 2018 · 51 comments · May be fixed by #121
Closed

Changing null -> undefined potentially dangerous #65

0x24a537r9 opened this issue May 30, 2018 · 51 comments · May be fixed by #121

Comments

@0x24a537r9
Copy link

At Facebook, we recently adopted a subset of ?. in our codebase, but the codemod from idx (see https://github.com/facebookincubator/idx) to ?. has introduced a number of bugs related to ?. always returning undefined, even when the original value was null. I see in the proposal:

Why does (null)?.b evaluate to undefined rather than null?
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.

In practice, this has quickly been found pretty unsafe, with the move away from idx's null-preserving style bringing a number of pretty significant and non-obvious bugs. I'd like to propose switching ?. to follow a more idx/||/??-like model of returning the first nullish value (null or undefined) rather than always undefined to avoid 4 main classes of bugs that result from this (likely unintentional) value conversion:

  1. Invariant warnings and exceptions. Relay for example, warns about passing undefined values as props ("Expected prop x to be supplied to Relay(MyComponent), but got undefined. React throws an invariant exception if you return undefined from a render function (which led to an app crashing bug at Oculus).
  2. Semantics changes with defaultProps and function parameter defaults (undefined will use the defaults, whereas null will be passed through as null)
  3. Semantics changes with equality comparison:
    3.a. when they're intentionally treated differently (because undefined represents the nonexistence of a value but null represents a value of nonexistence)
    3.b. when they're unintentionally treated differently (this caused another major bug internally when !== null was used instead of != null)
  4. Differential coercion. Number(null) === 0 but Number(undefined) === NaN

In all cases, the unintuitiveness of ?. converting nulls to undefineds can cause (and has already caused) major bugs in otherwise sensible-looking code. Are there reasons not to go with a semantic-preserving system like idx and ||, which seem to be a bit safer?

@fishythefish
Copy link

fishythefish commented May 30, 2018

Do these problems exist only because we're codemodding away from something which uses a different convention, or is undefined genuinely wrong in some cases? Producing undefined whether the LHS is null or undefined seems consistent with the idea that undefined represents the nonexistence of a value and that null represents an "empty" value.

Looking at it another way, if we could rewrite the semantics of idx, what are the cases in which idx producing null would be desired behavior (with respect to language features as well as the intended semantics of null vs. undefined) rather than just a convention to follow? If idx had always produced undefined instead of null, would all the listed problems disappear?

The change from null to undefined is gonna get in the way of automatically converting code that already uses a stopgap with a different convention, but IMO is correct with respect to the semantics of null and undefined.

EDIT: The obvious counterargument is that if we treat null and undefined as having different semantics, then there's a reason why the LHS is null instead of undefined (or vice versa). Therefore, it may be reasonable to ask ?. to propagate this information, but I'm curious where and how often this arises in practice.

@0x24a537r9
Copy link
Author

It's a fair question, and one I asked myself before posting, but yes, I do think these are actually intrinsically problematic, not simply because of existing precedent of idx. I'm not sure I follow the logic of:

Producing undefined whether the LHS is null or undefined seems consistent with the idea that undefined represents the nonexistence of a value and that null represents an "empty" value.

Could you clarify? If I have a.b?.c and a.b is nullable but required, I would expect a.b?.c could be null but not undefined because a.b is only supposed to allow empty values, not nonexistent values. That is, I think 'emptiness' vs. 'nonexistence' should be preserved the same way it does with || and ?? (even if we ignore idx).

Re: rewriting idx, again a very good way to look at this, but I still think my proposal holds. Fundamentally, I see idx(a, _ => _.b.c) and a?.b?.c as replacements for what we all used to do: a && a.b && a.b.c. In that case the nullish value is preserved. Some examples of the kinds of bugs we're finding that because that's not what happens and it's not obvious:

  • nullOrString?.field !== null will always pass even if nullOrString is null (we've found a dozen such bugs and their contrapositives with a quick grep of our codebase)
  • Number(nullableOrUndefined?.numericString) will return the field or NaN rather than 0 (we've found a dozen such bugs too)
  • nullOrObject?.boolField && <Component ...> can be undefined, which can cause React to throw (causing at least two crashes so far)
  • Calling foo(nullOrObject?.number) on function foo(nullOrObject = {number: 5}) {...} will unexpectedly trigger the default param
  • Using <MyComponent optionalField={nullOrObject?.number}/> where a field prop in React has a defaultProp will unexpectedly trigger the default prop (we've found at least one case of this in our code)

These are all cases where null !== undefined and a reasonable engineer might expect null but get undefined. The last two cases in particular end up changing the concept of an empty value to a nonexistent value in an entirely nonobvious way. I think propagating the nullish value is more expected and consistent with || / && / ?? / a && a.b && a.b.c in JS and less likely to be a bug factory in new code.

@0x24a537r9
Copy link
Author

Thinking some more, I think it basically comes down to whether you expect a?.b to expand recursively as a && a.b or (a || {}).b, but it can't actually be the latter because that would not short-circuit, which I think we can all agree is desired.

@fishythefish
Copy link

Sure, reducing to the simple case of a?.b, the point is that as long as a is nullish, whether a is null or undefined, a.b is a value that does not exist, not a value that exists in an empty state. Therefore, a?.b should produce undefined, not null. This is essentially the argument given in the proposal.

As I mentioned above, one feasible counterargument is that, since there is a semantic distinction between null and undefined, having ?. only produce undefined in the nullish case discards information about the LHS which we might want to preserve instead. (Alternatively, we could understand null to mean "empty" not in the sense that it has no properties, but that all of its properties are themselves in this null state.)

|| and ?? are hard to compare with ?. because they don't perform property accesses; they simply select one of two choices you provide as operands. If one of these operators converted a null operand to undefined, that would indeed be surprising behavior. ?. doesn't convert its LHS from null to undefined; it says that performing a property lookup on a null value should give you an undefined result.

@ljharb
Copy link
Member

ljharb commented May 30, 2018

@0x24a537r9 I think the question is, if you authored your code so it never distinguished between null and undefined (which is a common approach in the JS ecosystem), would this concern still apply?

@ramesaliyev
Copy link

Thinking some more, I think it basically comes down to whether you expect a?.b to expand recursively as a && a.b or (a || {}).b, but it can't actually be the latter because that would not short-circuit, which I think we can all agree is desired.

@0x24a537r9 But how you gonna distinguish between null is being value of b or a? I think this distinction is necessary. When I do const x = a?.b, i expect that x will be either value of b (if b exist) or undefined (so it isnt exist or really undefined, which is the same thing in most cases and i should've used null for empty values in the first place.).

sorry for the bad english btw.

@0x24a537r9
Copy link
Author

0x24a537r9 commented May 30, 2018

@fishythefish Thanks for clarifying. I see your point, and I think it's a reasonable interpretation of the operator. I think the thing is whether you expect the operator to be "about" the final value or "about" the navigable chain. I expect the latter given &&, but I think both are reasonable interpretations in the abstract. I think it's up to what "most" devs expect, which I'm not sure of.

What I like about the &&-based interpretation is that it's very simple to polyfill, which reflects a general coherence with the language paradigms, I think. Specifically, when tmp is a scratch variable you can ensure won't collide with another existing variable, lhs?.rhs can always be recursively expanded as a simple macro to be exactly identical to (tmp = lhs ?? tmp.rhs).

@ljharb Yeah, the issue is that JS itself distinguishes between null and undefined:

  • Number(null) === 0 && Number(undefined) === NaN
  • If const foo(a = 10) => a; then foo(null) === null && foo(undefined) === 10)
  • If const {a = 10} = x; then when x = {a: null} you'll pull out null, but when x = {a: undefined} you get 10

It is also common and useful to distinguish between null and undefined in core libraries as a way to enforce that the dev is intentionally (as opposed to unintentionally) returning an empty object.

  • React throws an invariant exception when you return undefined from a render function, which is valuable to catch when you fall-through a render function.
  • Relay warns when you pass undefined as a variable, which is valuable to catch when you accidentally failed to specify a variable.

And there are also differences when you interoperate with other languages that don't support undefined:

  • When marshaling data across a JS bridge to a native layer with JSON.stringify(), the other end usually drops undefined values (which is arguably correct, but is nonetheless perhaps unintentional)

@ramesaliyev That is an issue, yes, but you have the same thing with undefined under the current spec, so I'm not sure that's a concern. Specifically, in the current proposal you're losing information about whether a is undefined or b is, so I see those points as moot.

@claudepache
Copy link
Collaborator

claudepache commented May 30, 2018

What I like about the &&-based interpretation is that it's very simple to polyfill.

None of the two interpretations is harder to polyfill than the other. You use the transformation
a?.b.c().d idx(a, _ => _.b.c().d), where either:

const idx = (x, c) => x == null ? x : c(x);

or:

const idx = (x, c) => x == null ? undefined : c(x);

@0x24a537r9
Copy link
Author

@claudepache uhhh... lemme think... yes. Yes, you're correct, except in the case of idx(a, _ => _), where the latter would convert to undefined if a is null, contrary to the current spec for ?., although that isn't syntactically possible if ?. is a binary operator rather than a function, so meh. That is to say, the former does not treat the final property differently, whereas the latter does, but only needs to be accounted for when c is the identity function which is ignorable. Never mind then on that point then.

@ljharb
Copy link
Member

ljharb commented May 30, 2018

React throws an invariant exception when you return null from a render function, which is valuable to catch when you fall-through a render function.

It's the opposite; null is valid, undefined has the exception.

All of the cases you mention for differentiating are valid; but i wouldn't use optional chaining with those - and specifically, in all those cases, the explicit undefined check is done before attempting to navigate off of the object - and that navigation could use optional chaining.

@0x24a537r9
Copy link
Author

@ljharb whoops, yes. I need more sleep. Edited that comment to reduce confusion.

The thing is that while you indeed may not, the fact that you're in a TC39 conversation means that you're probably a lot more knowledgeable and conscientious about JS than the average dev ;) The average dev probably will unwittingly pass in a ?.-created value in all of those cases at various times and will be mystified when nullable?.field produces (from a naive perspective) undefined out of nowhere--we're already seeing cases in our codebase. How much more confused will that average dev be when they see the fix: nullable?.field ?? null. I don't think it's difficult to imagine the average dev thinking "oh! That's redundant. Lemme just remove that," and somewhat reasonably so.

What I'm proposing is a pragmatic approach to how devs will probably end up using the operator rather than how they theoretically should. I think I buy @fishythefish's argument that undefined is somewhat more abstractly consistent for one interpretation of chained ?.s, but when a && a.b && a.b.c is so common as a pattern, I think following that pattern's behavior precedent is more ergonomic and safe (I'd venture to guess that's why idx does it that way.) typeof NaN === 'number' and 'string' instanceof String === false may be abstractly valid and make sense if you really think about them, but are clear footguns. I worry about the (naive appearance of) conversion of null to undefined here also being a footgun--something you always have to watch out for rather than something that encourages robust code.

What do you mean by "the explicit undefined check is done before attempting to navigate off of the object - and that navigation could use optional chaining."? I didn't quite follow. Maybe an example would help?

@ljharb
Copy link
Member

ljharb commented May 31, 2018

// pretend this is inside React
var result = renderComponent();
if (typeof result !== 'undefined') { throw new Error(); }
var nodeType = result?.type; // either `undefined` if result is null, or `result.type`

@littledan
Copy link
Member

@0x24a537r9 Thanks for your detailed feedback here. It's incredibly valuable to see the result of the work you've put in to prototype ?. at scale. I hope we can get more feedback like this in the future.

The semantic alternative of returning null rather than undefined in this case seems reasonable to me, even as the original logic of always returning undefined has its logic to it as well. Let's keep thinking about these two alternatives; maybe this question warrants raising to the committee to get a broader perspective.

Note that, if we go with the "null-preserving" semantics, then for cases which want undefined, you can always use the nullish coalescing operator: a?.b ?? undefined.

@jrista
Copy link

jrista commented Jun 27, 2018

"I think I buy @fishythefish's argument that undefined is somewhat more abstractly consistent for one interpretation of chained ?.s, but when a && a.b && a.b.c is so common as a pattern, I think following that pattern's behavior precedent is more ergonomic and safe (I'd venture to guess that's why idx does it that way.) typeof NaN === 'number' and 'string' instanceof String === false may be abstractly valid and make sense if you really think about them, but are clear footguns. I worry about the (naive appearance of) conversion of null to undefined here also being a footgun--something you always have to watch out for rather than something that encourages robust code."

@0x24a537r9

I don't know that it is correct to call this a "conversion of null to undefined". I think @fishythefish's explanation of the ?. operator is quite valid as he described it. If you have a?.b, if a is null, null has no properties, therefor b cannot be defined on null. Therefor, we are not converting the value null of b to undefined...and, we are not converting a as null to undefined. We are simply saying that "b is not defined on a, as the value of a is null."

That, at least, is how I would read that statement. When I write a?.b, I am not looking for the value of a...I am looking for the value of b, if it and by extension it's entire parent chain, exists.

@Mouvedia
Copy link

When I write a?.b, I am not looking for the value of a...I am looking for the value of b

This.

@0x24a537r9
Copy link
Author

@jrista Yeah, I think that's a very reasonable interpretation. And you're right it's not really "converting" anything--just destroying information. I meant it more in the sense of @fishythefish's original comment:

The obvious counterargument is that if we treat null and undefined as having different semantics, then there's a reason why the LHS is null instead of undefined (or vice versa). Therefore, it may be reasonable to ask ?. to propagate this information, but I'm curious where and how often this arises in practice.

Out of curiosity, I posted this poll to r/javascript to see whether the average dev will expect ?. to act more like the null coalescing operator (which propagates this information) or more like property accessing (which is simpler/more consistent). If it turns out that most devs expect undefined vs. the first nullish value, I'm happy to concede that's the way the standard should be written 😄

@Zarel
Copy link

Zarel commented Jun 27, 2018

The poll (and reaction emoji here) seem to pretty decisively favor undefined.

image

I'd like to defend @ramesaliyev's point. @0x24a537r9, you write:

That is an issue, yes, but you have the same thing with undefined under the current spec, so I'm not sure that's a concern. Specifically, in the current proposal you're losing information about whether a is undefined or b is, so I see those points as moot.

JavaScript currently loses information about whether or not variables are undefined rather frequently – {a: undefined}.a === {}.a, f = (x = 10) => x; f(undefined) === f(), etc. On the other hand, JavaScript rarely loses information about whether or not variables are null, and that seems a useful thing to preserve.

It's pretty easy to detect whether or not a is null: a === null, and it would be nice to also be able to also easily detect whether or not a.b is null with a?.b === null. It would be surprising for it to work any other way, because JavaScript otherwise very consistently provides undefined in situations where a value is unavailable.

@Zarel
Copy link

Zarel commented Jun 27, 2018

Let me put it this way:

function f(arg) {
   arg === 1 // f(1)
   arg === null // f(null)
   arg === undefined // ambiguous: f() or f(undefined)
}

a[b] === 1 // element b exists and is 1
a[b] === null // element b exists and is null
a[b] === undefined // ambiguous

a?.b === 1 // a exists and has property b, which is 1
a?.b === null // a exists and has property b, which is null
a?.b === undefined // ambiguous

In all these situations, only === undefined is ambiguous. In the past, === null has never been ambiguous, and it would suck for that to change.

@Mouvedia
Copy link

Mouvedia commented Jun 27, 2018

@Zarel

function f() { console.log(arguments.length) }
f(undefined); // 1

@ghost
Copy link

ghost commented Jun 27, 2018

I'm logically onboard with the op always returning undefined, but struggle to see a downside in the return value being null | undefined. It preserves information at no cost that I can see. Can anyone provide a downside here?

@ljharb
Copy link
Member

ljharb commented Jun 27, 2018

@markkahn as has been explained upthread, it loses the information that the object is null.

@ghost
Copy link

ghost commented Jun 27, 2018

@ljharb -- I believe you misread my statement. I'm asking what the downside to null | undefined as a return value is

@lhorie
Copy link

lhorie commented Jun 27, 2018

I don't really understand the argument that a proposal should match behavior of existing idioms (e.g. a && a.b). If it does, then one can argue that all those crashes could easily have been prevented by not doing anything at all, and that the whole proposal might as well be dropped.

To me, codemodding a && a.b into a?.b without a care for null-vs-undefined semantics is equivalent to codemodding a FunctionExpression into a ArrowFunctionExpression without caring about this semantics.

I'm asking what the downside to null | undefined as a return value is

The downside is that typeof null === 'object'

Consider:

type Project = {
  owner: ?User
}
type User = {
  id: number
}

async function findProjectOwner(projectId) {
  const project = await get(`/api/project/${projectId}`); // may return null 
  if (typeof project?.owner === 'object') {
    return get('/api/user/${project.owner.id}');
  }
}

@jrista
Copy link

jrista commented Jun 27, 2018

I think it needs to be asked, here, what is the operator really doing? IS the operator in the case of a?.b doing a && a.b? Or is the operator doing a ? a.b : undefined? Or is the operator doing something else? What should the operator be doing, to be semantically correct, according to the definition of the operator? How does the javascript operator compare to how other languages implement the same operator...is it the same/similar, or is it different? (i.e. is it "familiar"?)

The propagation of information would need to be an explicit trait of the operator. I mean, what if a were some other value, other than null but not undefined, yet still falsy? If a was false, what should a?.b return? If a was 0, what should a?.b return?

Again, I would be looking for the value of b here, not the value of a... If the above expression returned false, when b was supposed to be a string, or a date, is that "correct"? It doesn't feel correct, to have any falsy value of a propagate into b...and in fact I think it could lead to unexpected problems and confusing bugs.

Is the desire to propagate null unique to nulls? If we did prop just null, but not any other falsy value, that seems to be a highly specialized case. Is it truly the "natural" expectation? (I don't know that I would bet on that... ;))

Should we resort to a && a.b if information propagation is the desired behavior (which is not particularly verbose, and already works this way), and leave a?.b operating as the spec currently defines it's behavior (to return undefined if a is null)?

@ljharb
Copy link
Member

ljharb commented Jun 27, 2018

a?.b.c is doing a == null ? a : a.b.c

This issue is asking it to do a === void 0 ? a : a.b.c

@jrista
Copy link

jrista commented Jun 27, 2018

@ljharb: This is why I asked, to see what people think is the case. ;) That is not what it is doing. According to the spec in the main readme:

https://github.com/tc39/proposal-optional-chaining#base-case

a?.b.c is doing a == null ? undefined : a.b.c

Someone posted a poll earlier in the thread, and it appears the above was overwhelmingly chosen by the community. That indicates this is how most javascript developers expect the operator to work. This is the logical and semantically correct outcome as well.

@ljharb
Copy link
Member

ljharb commented Jun 27, 2018

ha, i see what you mean. my gut reaction is as above; but now thinking more about it, by returning undefined always, it’s giving you information about b instead of a, which i think might be desired here - hence the current behavior.

@0x24a537r9
Copy link
Author

@jrista The closest analog to my proposal was more a ?? a.b ?? a.b.c, which only coalesces nullish values.

@ALL:

With now twice the data, I think the answer about what is "more expected" is pretty clear:

image

I think this poll (as unscientific as it was) clearly demonstrates at least two facts:

  1. I was wrong. We should indeed go for the original a == ? undefined : a.b approach unmodified--I'm persuaded.
  2. That said, with almost 1 in 3 of those who recognized what the operator expecting a different result (null), we should probably take extra care in the wording of this operator when it rolls out so as to minimize surprises. Not sure what we can really do except be extra clear in documentation.

Thanks all for an enlightening conversation! Feel free to close unless there are any further thoughts!

@ghost
Copy link

ghost commented Jun 27, 2018

@lhorie -

The downside is that typeof null === 'object'
...
if (typeof project?.owner === 'object') {

My argument to this is the same one you made -- "I don't really understand the argument that a proposal should match behavior of existing idioms"

That line can easily be replaced with:

if (project?.owner === null) {

In other words yes, it's different behavior but I don't see any ways in which it's worse

@lhorie
Copy link

lhorie commented Jun 27, 2018

@markkahn if (project?.owner === null) is absolutely not the same thing at all. The primary intent of the original check in that example is to conditionally do a side-effect if owner is an object. It's not meaningful to attempt to get a user without an id.

To me, it seems extremely bad that typeof some?.deep?.object?.path === 'object' if anything in the chain is null. Brendan Eich already said typeof null was a bug that we're now stuck with. Loose, buggy and infectious semantics is just asking for trouble.

@ghost
Copy link

ghost commented Jun 27, 2018

@lhorie -- Sorry, I misunderstood your original intent. Still, != null seems to solve the issue. It only fails when you're in a situation where project?.owner has multiple possible types (e.g. object and... number? string?), and I'd argue that that would be the issue in itself and that the author would require stronger type enforcement, not that this proposed change returns typeof === 'object'

@lhorie
Copy link

lhorie commented Jun 27, 2018

Still, != null seems to solve the issue

It's not equivalent semantically exactly for the reason you described. You're replacing a whitelist check (i.e. only objects) with a blacklist (not nulls and not undefineds). Generically speaking, you're suggesting replacing a more strict check with a less strict one. Doing that would be similar to blindly codemodding === checks with == checks, which is clearly and objectively worse.

I'd argue that that would be the issue in itself

That can be said about many of the regressions the OP experienced. Regressions due to reliance on Number(infectiousFalsy) === 0, for example, falls into the same category of being too fast-and-loose with types and then getting bitten in the ass down the road, IMHO.

I see it as a feature that null?.b === undefined discourages questionable downstream coercions such as Number(null) in favor of more strict checks such as typeof val === 'string' ? Number(val) || 0 : 0

@btecu
Copy link

btecu commented Jun 28, 2018

I'd like to point out that C# will return null if that's the value of a in a?.b.
I know C# doesn't have undefined but I'd expect Javascript a?.b to return null if a is null.

@claudepache
Copy link
Collaborator

Although it is still unclear for me that the concrete hazards raised in the present thread would not just be inevitable refactoring hazards, I do think that having (null)?.b === undefined is a mistake. See #69.

@cloudkite
Copy link

I like the behaviour of returning undefined always.

This makes comparisons easier for example a?.b === c?.b is much simpler to reason about when undefined is always returned.
to get the same behaviour when both null and undefined can be returned I would need todo something awkward like (a?.b ?? null) === (c?.b ?? null)

@jrista
Copy link

jrista commented Aug 16, 2018

to get the same behaviour when both null and undefined can be returned I would need todo something awkward like (a?.b ?? null) === (c?.b ?? null)

Couldn't you just do:

a?.b == c?.b

With a nullish check? That should cover null and undefined, but not other falsy values, right?

@cloudkite
Copy link

@jrista not if you want to avoid all the pitfalls of javascript type coercion https://dorey.github.io/JavaScript-Equality-Table/

@cloudkite
Copy link

for example if a?.b evaluates to false and c?.b evaluates to 0 I dont want the expression to return true which is what would happen with a?.b == c?.b

@ljharb
Copy link
Member

ljharb commented Aug 17, 2018

@cloudkite if it returns null or undefined, (a?.b ?? null) === (c?.b ?? null) is how you'd treat them the same, and a?.b === c?.b is how you'd treat them differently; if it always returns undefined, how would you write both of those use cases?

@cloudkite
Copy link

@ljharb personally I write my code to not distinguish between null and undefined. So I don't have a usecase for treating them differently. Maybe if you really want to distinguish between null and undefined you need to fallback to ternaries? I think we should optimise for the most common case. I hope the most common case is not distinguishing between null and undefined 🙏. But if the opposite is true then I'll make my peace with it 😊

@ljharb
Copy link
Member

ljharb commented Aug 17, 2018

I’m asking because i think it’s important to weigh the relative difficulties of both, in each of the scenarios.

@jhpratt
Copy link

jhpratt commented Aug 17, 2018

Here's my argument for undefined over null, with a practical use case. It does involve two other proposals, but they are directly tied to the outcome of this discussion.

The value (null or undefined) will be the same value used in the nullish coalescing operator. This is implied when it refers to this operator in its README. This operator will also be used as an optional assignment operator, per the March TC39 meeting.

One of the use cases I see for the nullish coalescing operator is assigning multiple properties onto an instance. It gets quite messy to see a dozen lines of if (opts.foobar !== undefined) { this.foobar = opts.foobar; }. I think everyone can agree it would be much cleaner to use this.foobar ??= opts.foobar (substitute the operator to whatever is decided on).

Let's say the option is a foreign key. null is a value valid for it ("no reference"), and should be assigned. undefined, on the other hand, would indicate an absence of the value in that particular request, such that it should not be set (keep the status quo).

If the optional chaining operator decides on null, the nullish coalescing operator will lose this use case, as you'd still have to explicitly check if it's undefined (not on the object in question). If it's null, we know that the user wants to remove the foreign key. If it's undefined, we know we should leave it alone.

To me, this is a perfectly valid use case (assigning properties on an instance in a REST API). I'm currently working on something nearly identical to this, and found it to be a great example of where the value in question would matter. In this instance specifically, it's inevitable that the decision was made to treat null and undefined differently.

Thoughts?

@jrista
Copy link

jrista commented Aug 20, 2018

I agree that there are useful and nuanced differences between undefined and null. In many cases they can be treated the same, however in some cases, they are not treated the same by runtime or platform. One of my favorite use cases is when I need to control what data is serialized out of a REST service. Undefined will not serialize, whereas null will. An often useful distinction.

If using express and node, for example, which runs on V8. With V8, any time you delete a property off an object, you force the underlying runtime to create a new low level class that represents the new shape of the object. Sometimes I may make several requests to various backend services, then spread the various results together. I often do not want the entire merged result being sent across the wire. For efficiency purposes, so I don't force V8 to spit out a bunch of new runtime types every time I delete a property, I simply set them to undefined, sometimes as the result of an expression (say an optional chain?)

This is then the most efficient way to prune a result from a REST service. Set properties I do not wish to serialize to undefined. Anything else, if there is no "valid" value, but I still need to know that on the client side, they become null.

Undefined and null are not the same thing. They are different, and sometimes those differences matter. Even if in a majority of cases they do not, we shouldn't lose sight of the fact that they are indeed not the same thing.

@nick-michael
Copy link

nick-michael commented Oct 16, 2018

One of the key comments on why we should be using null seems to be related to ambiguity, but I would argue both methods would add some element of ambiguity:

So take for example the undefined approach:

a = null;
a?.b    // undefined

a = undefined;
a?.b    // undefined

Here we lose information about the fact that a was null.

Now take the null approach:

a = null;
a?.b    // null

a = { b: null };
a?.b    // null

We still lose information about a and b, because where you might assume that a is null, it's actually an object.

The key question of this thread seems to be whether the operator is trying to find information about a or b, and, I would argue that due to the fact that it will return that value of b if it exists, and undefined if not, we are clearly asking questions about the value of b.

There is actually no situation that we return the value of a when using the ?. operator, so I would propose that if a === null then a?.b === undefined because b actually doesn't exist as a value (and as has been mentioned here, null is the existence of no value, and undefined is the non-existence of a value)

@noppa
Copy link

noppa commented Oct 16, 2018

@nick-michael You can avoid move elsewhere the ambiguity with null-preserving style where you need to.

a = null;
(a ?? undefined)?.b    // undefined

a = { b: null };
(a ?? undefined)?.b    // null

You can't easily do that with the "always undefined" style.

@ljharb
Copy link
Member

ljharb commented Oct 16, 2018

@nick-michael imagine a?.b is like a == null ? a : a.b in the null case, and a == null ? undefined : a.b in the undefined case, the former of which is already a very common pattern in js codebases.

@noppa
Copy link

noppa commented Oct 16, 2018

I don't know... I have seen all sorts of patterns used for this purpose.
Good ol' (a || {}).b, for example, aligns more with the "always undefined"-style.

@nick-michael
Copy link

nick-michael commented Oct 17, 2018

@nick-michael You can avoid move elsewhere the ambiguity with null-preserving style where you need to.

a = null;
(a ?? undefined)?.b    // undefined

a = { b: null };
(a ?? undefined)?.b    // null

You can't easily do that with the "always undefined" style.

I guess it depends what you would classify as "easily"

Personally I feel like: a === null ? a : a?.b is not a whole lot more complex than (a ?? undefined)?.b even though that's assuming the use of null coalescing (which I guess is fair enough)

BUT I would have to admit that when you start going another level deep, it starts to get a bit more horrible when using the lambda expression, although both get pretty bad:

a === null ?
    a 
    : a?.b === null ?
        a?.b
        : a?.b?.c


((a ?? undefined)?.b ?? undefined)?.c

Edit: Can be simplified further as pointed out

@nick-michael
Copy link

nick-michael commented Oct 17, 2018

Arguments can really be made both ways (as we've seen here!), but from my point of you the strongest case is the one I mentioned previously in that the optional chaining operator consistently returns the value of b, so if a is null, it only makes sense that as b is undefined and that's what gets returned

I also saw your comment in #69 with the table of current utility libraries which I think is a pretty strong case for what most JS developers would expect the outcome to be

@noppa
Copy link

noppa commented Oct 17, 2018

((a ?? undefined)?.b ?? undefined)?.c

can be simplified to

(a.?b ?? undefined)?.c

That said, I still prefer the always-undefined semantics to a small degree, just because it feels more intuitive to me and the way I see this operator. Personally, I don't think I have ever needed to disambiguate between a null intermediate value and an undefined "final" value. And if I ever need to, all the more verbose (and flexible) tools that we use today would still be there to help.

The "feels more intuitive" part is highly subjective, of course. I'd be happy with either one.

sendilkumarn pushed a commit to sendilkumarn/proposal-optional-chaining that referenced this issue Jun 22, 2019
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.
@claudepache
Copy link
Collaborator

Now that the proposal has reached stage 4, the semantics of (null)?.b is fixed.

Indeed, conversion between idx and optional chaining ought to be done carefully, as they are subtle semantics differences, the problem raised here is only one of them.

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.