Non-nullish coalescing assignment operator

TL;DR It is an operator that parallels with map.emplace(key, { update }), but for ordinary references (particularly, for objects with lookup purpose).

What?

We currently have:

value ||= defaultValue;

and

value &&= replacementValue;

The above are totally fine if value won’t ever be 0’s and ""’s (or if they really indicates an “absence”, but I believe it is seldom the case).

But what if not?

value ??= defaultValue;
value ⬚= replacementValue;

What fits the ⬚ in the second line?

Currently, we would have to write if (value != null) value = replacementValue; or value == null ? undefined : replacementValue, which is verbose and unergonomic. This is true even with the Nullish unary operator `?` (if (?value) value = replacementValue;).

Solution: The non-nullish coalescing assignment operator

In real world code, Object.create(null) (or even just {}) is used instead of new Map() for various reasons. The obvious one is the lack of equivalent of setdefault in Python, leading to the proposal of the emplace method on Maps.

const map = Object.create(null);
// insert
map[key] ??= [0];
// update
map[key] !!= map[key].sort((a, b) => a - b);

On the other hand, writing

value &&= normalizeOption(value);

is likely to be a bug as it doesn’t normalize 0’s and ""’s. The new operator prevents potential bugs due to falsy values.

The actual operator is to be bikeshedded, since it conflicts with TypeScript’s non-null(ish) assertion operator (it is mostly useless to write such an idempotent operator twice though) or in case the Non-null assertion operator (aka Swift's force unwrap) is pushed forward and is allowed on the left-hand side.

Another point for discussion is that whether the non-nullish coalescing operator !! should be in the proposal. I am not sure if writing a == null ? undefined : b as a !! b really worths.

Example

The following example is in TypeScript so as to better convey the idea behind:

In an external API

export function createModal(): Modal {
    // ...
}

export function displayMessage(
    modal: Modal,
    message: string,
    color: "black" | "gray" | "white" | number | undefined = prefersColorScheme() === "dark" ? "white" : "black"
): void {
    // ...
}

In the application

import { createModal, displayMessage } from "some-external-api";

type RGBColor = number;
type NonLegacyLiteralColors = "black" | "gray" | "white";
/** @deprecated */
type LegacyLiteralColors = "BLACK" | "GRAY" | "WHITE" | "GREY" | "grey";
type AllColors = LegacyLiteralColors | NonLegacyLiteralColors | RGBColor;

function normalizeColor(color: AllColors): NonLegacyLiteralColors | RGBColor {
    if (typeof color === "number") return color;
    color = color.toLowerCase() as "black" | "gray" | "grey" | "white";
    return color === "grey" ? "gray" : color;
}

export function showModal(
    message: string,
    options: { color?: AllColors | undefined } = {}
) {
    const modal = createModal();

    let { color } = options;
    // `&&=` is not suitable since `color` may be 0x000000
    color !!= normalizeColor(color);
    displayMessage(modal, message, color);
    // or
    const { color } = options;
    displayMessage(modal, message, color !! normalizeColor(color));
}

The ??= operator is already part of JavaScript. Nullish coalescing assignment (??=) - JavaScript | MDN

Or am I missing something?

They want to perform the assignment if value is non-nullish; ??= performs the assignment if value is nullish.

1 Like

I think that for this particular example, it would be way better if normalizeColor just accepted and returned nulls.

If you still feel you need this, I'd suggest an alternative syntax:

color =? normalizeColor(color);

The idea is based on optional chaining: in foo.?bar, member access happens if foo is non-nullish. In foo =? bar, the assignment would happen if foo was non-nullish.

In most examples, optional chaining can be used to solve the cases effectively.

map[key]?.sort((a, b) => a - b); // Note: Array.prototype.sort is in-place
color = color?.normalize(); // Only if the method exists on color

Of course, there are many situations where methods may not be available. However, if the Extensions proposal is implemented in the future, you could write an extension method and use it like color?.ColorExt'normalize() (syntax is yet to be determined, but it should be quite similar).