
Despite its crazy name, the nullish coalescing assignment operator ??=
is a handy tool. Let's break its name down and see how we might want to use it.
Nullish
Nullish is defined as a value being undefined or null. It is important to differentiate a nullish value from a falsy value.
// GIVEN... const a = false; const b = 0; const c = null; const d = {};
I use the concept of nullable all of the time in my typescript code. I have a type Nullable<T>
to indicate that a variable can have a value or be nullish (null or undefined). Here are my definitions:
1type Nullable<T> = T | null | undefined;
2type NotNullable<T> = T extends null | undefined ? never : T;
3type IsNullish = null | undefined;
4
5function isNullish(obj: unknown): obj is IsNullish {
6 return typeof obj === 'undefined' || obj === null;
7}
8function isNotNullish<T>(obj: unknown): obj is NotNullable<T> {
9 return !isNullish(obj);
10}
I use the Nullable<T> type anytime a variable (or property) might not have a value. The types NotNullable<T> and IsNullish are helper types for my type guard function isNullish
(which I use all of the time) and isNotNullish<T>
.
Consider the following class...
1class Person {
2 name!: string;
3 optIn: Nullable<boolean>;
4
5 constructor(name: string) { this.name = name; }
6}
The optIn
property has three states:
- true 😄
- false 🥹
- nullish ❓(not set)
The latter suggests that the Person has not decided if they want to 'opt-in.' So why not just make the property optional: optIn?: boolean
. Two reasons, both relating to the use of undefined
.
First, I never compare anything against undefined
nor do I assign anything to undefined
. Perhaps this is a holdover from my early JavaScript days when one was warned thatundefined
might have been reassigned (here is an example of redefining undefined
). So after optIn
is given a value, how do you mark it as "not set"?
Secondly, I like using null
when I want to assign an "empty" value (not set). This is just a preference but person.optIn = null;
makes sense to me: "the person has not make a decision on whether they want to opt-in."
Coalescing
coalescing: come together to form one mass or whole.
Null-coalescing operator
I use the null-coalescing operator ??
all of the time when giving object properties default values. For example,
1class Options {
2 category: 'A' | 'B' | 'C';
3 threshold: number;
4 expansive: Nullable<boolean>;
5
6 constructor(obj?: any) {
7 obj = obj ?? {};
8 this.category = obj.category ?? 'A';
9 this.threshold = obj.threshold ?? 100;
10 this.expansive = obj.expansive;
11 }
12}
Here, the properties category
and threshold
are given default values if they do not exist in the object passed into the constructor.
For a long time, I used the logical OR operator ||
to assign default values, and it wasn't until a few years ago that I ran into my first problem:
1class Config {
2 optIn: boolean;
3
4 constructor(obj?: any) {
5 obj = obj || {};
6 this.optIn = obj.optIn || true;
7 }
8}
const alpha = new Config(); const beta = new Config({optIn: false}); const gamma = new Config(false); const delta = new Config(null);
Assignment Operator
So why do we need to have an assignment operator when we can just use the null-coalescing operator ??
I only recently learned of the nullish coalescing assignment operator and was fine going without. Looking back to the Options class, you can see that I could not use the assignment operator. However, there are many times when I pass in a config object and "fill in" default values. For example:
1type RunConfig = {
2 maxSpeed?: number,
3 duration?: number;
4}
5const defaultRunConfig: RunConfig = {
6 maxSpeed: 5,
7 duration: 60
8}
9function run(config: RunConfig = {}) {
10 config = {
11 ...defaultRunConfig,
12 ...config
13 }
14 //rest of the code
15}
While I still use this pattern, I have found that in many cases, there are only a few properties that are optional and that need default values.
1type Config = {
2 duration: number,
3 maxSpeed: number,
4 startSpeed?: number
5}
6
7run(config: Config) {
8 config.startSpeed ??= 0;
9 /* rest of code here */
10}
Here is another use case for the nullish coalescing assignment operator I used recently...
1class Connection {
2 private _db?: string;
3 get db() { return this._db ??= 'default database'; }
4
5 constructor(config: ConnectionConfig) {
6 /* NOTE: ConnectionConfig is defined elsewhere and it has an optional db property */
7 this._db = config.db;
8 }
9}
Ok - usually, I would just do something like this._db = config.db ?? 'default database';
Or maybe initialize _db
in its declaration. But this pattern works, too.
Finally
The nullish coalescing assignment operator is another tool in your arsenal for working with Nullable values. It is a shortcut when doing something like:
a = a ?? 'happy';
a ??= 'happy';
Test your understanding...
Consider the following function definition...
1type Nullish<T> = T | null | undefined;
2
3const mystery = () => {
4 let a = "a";
5 let b: Nullish<string>;
6 let c: Nullish<string>;
7
8 b ??= ( c ??= a );
9
10 console.log(a,b,c);
11}
What is the output of the function mystery() defined above?