tc39 / proposal-nullish-coalescing

Nullish coalescing proposal x ?? y

Home Page:https://tc39.github.io/proposal-nullish-coalescing/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Potentially unnecessary?

mvaldesdeleon opened this issue · comments

Based on the examples provided as motivation, I would raise that such behaviour can already be achieved using existing language features, and that there is no need to utilize the limited syntax budget with yet another operator just because people would rather write things how they are used to with a problematic operator (||).

To achieve the expected results, a simple combination of destructuring assignment plus default values should be sufficient, as the following example illustrates:

const response = {
  settings: {
    nullValue: null,
    height: 400,
    animationDuration: 0,
    headerText: '',
    showSplashScreen: false
  }
};

const {
    settings : {
        undefinedValue = 'some undefined default',
        nullValue = 'some null default',
        headerText = 'Hello, world!',
        animationDuration = 300,
        showSplashScreen = true
    } = {}
} = response;

The motivation needs to be expanded in order to present other use cases. But here are already some quick thoughts:

  • I find destructuring assignments with defaults painfully difficult to read (but maybe it’s because I’m not accustomed to that idiom?);
  • I want to be able to use fallbacks without assigning the result to something (so that destructuring assignment is not the right tool), as in: this.animate(animationDuration ?? 300);
  • I want to use fallbacks of values which are not properties of an object or an array (so that destructuring assignment is not the right tool), as in: let animationDuration = config.read('animationDuration') ?? 300.

But here is a real-world example. I have the following line in my code, where input is an HTMLInputElement instance:

input.value = input.defaultValue = item && item.alias_ext || ''

I want to be able to write:

input.value = input.defaultValue = item?.alias_ext ?? ''

but I am not interested in:

;({ alias_ext: input.defaultValue = '' } = item || { })
input.value = input.defaultValue

Without really taking a stance on this, a few thoughts about these examples:

In @mvaldesdeleon's original example, nullValue would still be null, not 'some null default' as I'd expect would be the intention there.

I don't think

input.defaultValue = item?.alias_ext ?? ''

is a very good example, because (assuming the type of alias_ext is null | undefined | string) you could still safely use

input.defaultValue = item?.alias_ext || ''

The only string that would fall back to the default case is the empty string.

A better example that demonstrates the need for this operator is having a default value that requires computation which we'd rather not do if the original value is not nullish and a default value is not needed (see #5).

For example,

input.defaultValue = item?.alias_ext ?? await fetchDefaultAliasExt(config.someOption)

Defaults only apply to undefined. This operator would apply to both undefined and null.

Additionally, destructuring with defaults - even if it wasn't often ugly - creates new variable bindings in scope, this operator does not.

I appreciate the need for short circuiting but the entire issue is solved with a single function. Underscore calls this property and Clojure calls it get-in.

var bob = {name: "Bob", address: {street: "211 Market St." zipcode: 17011}};
var tom = {};
var bobZip = bob?.address?.zipcode;
var tomZip = tom?.address?.zipcode;
var bobZipF = Object.getIn(bob, ["address", "zipcode"]);
var tomZipF = Object.getIn(tom, ["address", "zipcode"]);

I agree that some things deserve syntactic improvements, but not everything. Plus, having a function permits composition.

user |> Object.getIn(?, ["address", "zipcode"]) |> shippingCharge(order, ?)

It would almost be more useful to permit Ruby's w% for declaring arrays. The benefit of %w would be useful in other contexts as well.

user |> Object.getIn(?, %w(address zipcode)) |> shippingCharge(order, ?)

@mlanza that doesn’t work ergonomically when you want some of the accesses to be optional, but others to be required.

@mlanza You appear to be questioning ?. on the ?? proposal. But anyways, here are a variety of reasons to have optional property syntax built into the language.

  • Libraries/frameworks/applications should not have to install lodash/underscore/clojure just to get optional chaining.
  • The exact same argument that ?. can be done with user functions instead can be said for filter/map, trim, Object.assign, etc... which existed as user functions before they were adopted into the language. Just because you can do it with a library does not mean it has no value in the core language if it is a really common thing to do.
  • You might be able to get types working with something like Object.getIn(foo, ['bar', 'baz']), but you will not get IDE type completion for those property names to work but optional chaining has a chance of getting that to work. i.e. As soon as TypeScript gets ?. I expect that vscode will start treating ?. like . and output the same type based completion panel when you type foo?. as you get when you type foo..
  • Functions are generally slower than simple property lookups, it may not be that much but there is enough of an annoyance of replacing a foo.bar.baz with Object.getIn(foo, ['bar', 'baz']) that projects are liable to stick to foo || foo.bar || foo.bar.baz which is horrible. Also JS engines cannot optimize functions like Object.getIn, but they can optimize syntaxes like ?..
  • It is much much easier to write foo?.bar?.baz than it is to write Object.getIn(foo, ['bar', 'baz']) in every way. It's shorter, it has no unnecessary boilerplate, and it does not require moving your fingers to the parenthesis keys for just a property lookup.
  • It is much much easier to convert a bunch of foo.bar.bazs (in a large block of code where the issue is spread out amongst multiple lines that are similar but work with different properties) which you later realize needs optional properties to avoid errors to foo?.bar?.baz (it's a very trivial . to ?. search and replace) than it is to rewrite that code to be Object.getIn(foo, ['bar', 'baz']) which requires wrapping each of them in a function call and turning property lookups into an array of strings.
  • Not a big deal, but I expect ?. may be nicer to deal with than a function for the subset of users who use extreme minification that mutate property names.
  • It's partially a side note, but function based things like Object.getIn are not available in template+JS mix syntaxes like Angular. Angular has already essentially extended it's subset of JS just to add this operator into templates. It would be much nicer for this to just be a language feature and not require Angular to extend JS just to get it.
  • I understand that property lookups don't work with the fn(?) partial application syntax, but it's still trivially easy to use the optional property syntax with the pipeline operator. user |> (u => u?.address?.zipcode) |> shippingCharge(order, ?) in fact, it's shorter than your example!

I think I'll stop there at trying to come up with reasons.

Edit: Side note, since you like user-level utilities. I believe it is already possible to create user-level equivalent of Ruby's %w{} that is equally short and easy to use as that syntax is, with only the caveat you'll have to import a library. Just create a function that uses the template tag syntax. e.g.

import w from 'my-w-package';
w`address zipcode`

Though you can probably come up with a better name for it.

@dantman

I have in practice, in production code, for years, consistently used Object.getIn without, in my opinion, harm to legibility of the overall codebase. I code in a functional style and so composing/piping functions together is a natural fit.

I have never coded foo || foo.bar || foo.bar.baz. I only ever go one level deep (e.g. foo || foo.bar) or use Object.getIn.

There'd be no reason to install a lib if a single Object.getIn function was included in the core.

IDE help is not vital for me. I use Sublime Text and am content with the tradeoff.

My resistance is a general one where I feel that a language becomes unnecessarily complex when it does with syntax what can easily be done without. Each additional syntactic rule makes the language more cumbersome. Thus, I'm for sugar in only the most frequent use cases.

Hold your breath. I have read numerous posts about async/await as the true successor to promises, but have not found that syntactic enhancement necessary. Promises compose just as well as functions and with no harm to legibility.

Each syntactic improvement is your brain jumping through a hoop to understand how the reader is interpreting the code. When you stick to the fundamentals of composition, you no longer have to jump through those hoops. You just glue things together in a universally consistent way.

Clojurians tell you to avoid coding macros where a function will do. Syntax = macros.

@ljharb -- based on my experience with using Object.getIn over the years, I can't conceive of a single situation where once I go to optional access it becomes necessary and useful to revert to required.

@mlanza what that means is that either solution is satisfactory for you. what that does not mean is that the many use cases that do exist for needing that aren't valid; and the validity of those is why a function alone is not sufficient - thus, why optional chaining is.

However, this repo is for nullish coalescing, so perhaps you're arguing on the wrong repo, as #32 (comment) indicates? Try https://github.com/tc39/proposal-optional-chaining/

Closing, since this proposal is at stage 4.