'maybe' operator to complement ?? operator

Essentially: proposed maybe operator is a logical inversion of ?? operator.

Maybe operator a !? b : evaluate expression b if expression a is not nullish.

res = isNulish(a)  ? a : b;

Practical use:

var celsius; 
var fahrenheit  =  celsius !? celsius * 9 / 5 + 32;
// no error and fahrenheit is undefined as celsius is undefined too.

We simply need this, although the 'appropriate' syntax is &?. Using ?? for nullish coalescing was unfortunate:

In proposing the nullish coalescing operator ??, there is not much discussion aside from a reference to C#, from which the ?? syntax was borrowed.
However, this is shortsighted, since C# uses boolean logic, while ES uses truthy logic. We take advantage of the fact that ES && and || operators will return the actual value of the first operand when short-circuiting occurs, rather than true and false. C# cannot do this, and so is inappropriate to borrow from directly.

In the sense that && mathematically complements || in truthy logic, we should have added together the operators &? and |?. Here, |? is the proper name for what we today call ??, and &? is its complement. The usage of & and | as the first character of the operator is an obvious and natural allusion to the closely related && and || operators, and similarly connotes their complementary nature. Indeed, we can regard &? and |? as narrowing/specializing && and ||, respectively, so the naming is quite appropriate.

The common names for &? and |? can be simply "nullish AND" and "nullish OR", respectively. Note that where some references define && and || as "logical AND" and "logical OR", it should be clear by now that there are multiple types of "logic" at play (Boolean, nullish, truthy, etc.) and we should be specific. We love ES's truthy logic and are also now embracing nullish logic!

The need for the &? operator comes up in many cases, as you might expect being the proper mathematical complement of ?? (ahem.. |?). One example is in using DataLoaders in a GraphQL resolver, where one needs to guard that the key is not nullish, but allow zero as a key. We cannot use && and we would like to use &?.

({ key }) => key &? loader.load(key)

Without this, we resort to a silly type predicate function in TypeScript:

export const ok = <T>(value: T): value is Exclude<T, null | undefined> =>
  // double `not equal` is the operator we need here (`null == undefined`)
  (value != undefined || undefined) as boolean;

used as

({ key }) => ok(key) && loader.load(key)

Note that we had to force-cast the result in TypeScript to be able to use a truthy result where a boolean result is expected.

Unfortunately, we also defined ??= which carries on the inappropriate C# nomenclature.

There is also @claudiameadows's nullish checking pipeline idea (https://github.com/tc39/proposal-pipeline-operator/issues/159) as an alternative solution.

let fahrenheit  =  celsius ?> (c => c * 9 / 5 + 32);
down level compiles to:
let fahrenheit = (celsius !== null && celsius !== undefined)
 ? celsius * 9 / 5 + 32
  : void 0;

The question of pipeline operators should be treated separately. The proposed pipeline operator |> has not seen much traction yet. I expect primarily because ES does not curry its function arguments. There is then the whole question of pipeline operators for 2 and 3 arguments, hence the ||>, |||>, <|, <||, <|||, ..., as in F#. Quite a quagmire.

From my GraphQL resolver example, it often happens that the key is composite, coming from a destructuring. In that case, we immediately see that the pipeline needs to support multiple arguments to be useful:

({ key1, key2, key3 }) => 
  key1 &? key2 &? key3 &? loader.load({ key1, key2, key3 })

Also note, that there is no need to introduce a lambda expression and pipelines where an appropriate operator will suffice. The operator already short circuits and does not evaluate the right hand side in that case.

The pipeline operator has had a lot of discussion, but that is a good thing. We shouldn't rush things into the language. This idea does come up frequently, so it definitely feels like a problem worth solving. Hopefully someone on the committee agrees.

1 Like

@aclaymore Thanks for the references. I do hope the committee take this forward.

1 Like

I just signed up to make this proposition. Luckily I found this thread (and the others), so there's no need for another duplicate. I'd like to add that a common use case for this would be mapping values from external sources. This is what I'd like to do:

function mapIncoming(incoming: any): Model {
  return {
    created: incoming.created == null ? incoming.created : parseDate(incoming.created),
  };
}

But it's tedious and many teams would hesitate to make this their recommended style. The alternatives are equally inconvenient. You could have a rule that all your mapping functions need to be nullish safe. But that would lead to type widening and bloat the function code. Instead, using the proposed operator &?

function mapIncoming(incoming: any): Model {
  return {
    created: incoming.created &? parseDate(incoming.created),
  };
}

would be much more readable.

An addition like this into the language would be useful. While a concise operator like &? would be nice, perhaps it's worth exploring other more verbose options as well.

Due to limited ascii symbols available, adding new operators seems to be a fairly rare thing. There's a growing concern of JavaScript turning into an ASCII soup with all sorts of symbolic shorthands being used everywhere, which in turn can make code feel fairly cryptic.

Keyword operators or simply a new helper function might be easier to add - they're not as concise but they get the job done. There's actually plenty of prior art for this sort of thing if we don't hyper-focus on the operators and think about other options as well - in fact, pretty much any language that has a "maybe" monad would also have some function that goes along with it to let you operate on the value contained in the monad, but only if the monad contains a value.

Rust has an <option>.map() method.

Elm has <maybe>.map()

In Haskell you can do fmap over their maybe type.

etc.

And, sure, these all have to do with a maybe/option type which JavaScript doesn't have (and I don't think it would be a good idea to add one at this point), but we could still provide a "maybe"-like API without literally having a "maybe" type. And, for a dynamic language like JavaScript, that could be good enough.

So, for example:

// Nullish could be a new top-level namespace class (like `Math`)
// to hold various maybe-like API functions.
// `.map()` is just one possibility.
Nullish.map(null, x => x + 1); // null
Nullish.map(undefined, x => x + 1); // undefined
Nullish.map(2, x => x + 1); // 3

If we don't want to introduce a whole Nullish namespace class, we could still add a stand-alone helper function:

mapNullish(null, x => x + 1); // null
mapNullish(undefined, x => x + 1); // undefined
mapNullish(2, x => x + 1); // 3

After introducing this helper function, if it's a success, we could afterwards think about introducing a nice operator to go with it as a shortcut - kind of like how we have both Math.pow() and **. This operator could be as concise as &?, or it could be a "phrase operator" like "map nullish".

Maybe the pipeline operator |> could also help for this matter, so instead of writing:

function mapIncoming(incoming: any): Model {
  return {
    created: incoming.created == null ? incoming.created : parseDate(incoming.created),
  };
}

we would be able to write:

function mapIncoming(incoming: any): Model {
  return {
    created: incoming.created |> % == null ? % : parseDate(%),
  };
}

The nice thing is that in that case, we also spare the repetition of incoming.create for the second expression of the ternary operator.

Also, this way would allow to branch depending on other conditions, not necessarily on nullish values:

created: incoming.created |> isValid(%) ? % : fix(%),

Edit:

I saw that the pipeline operator has already been mentioned above in this thread, but my point about it is that it would allow to be more flexible about what we could achieve for that matter. You avoid that awkward expression repetition (and with it you can also avoid it for the second expression of the ternary), while been also flexible about the condition of the branching.

So actually I'm rather wondering whether we would specifically need such a specialized tool for this, when another one would potentially solve a more generic problem?