Nullish Coalescing Assignment
typescript-today

Nullish Coalescing Assignment

Great for initializing object...

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.

Test your `nullish` v. `falsy` knowledge

// GIVEN... const a = false; const b = 0; const c = null; const d = {};

Which of the following is true...

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:

typescript
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...

typescript
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,

typescript
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:

typescript
1class Config {
2    optIn: boolean;
3
4    constructor(obj?: any) {
5        obj = obj || {};
6        this.optIn = obj.optIn || true;
7    }
8}
Careful with Logical OR operator

const alpha = new Config(); const beta = new Config({optIn: false}); const gamma = new Config(false); const delta = new Config(null);

Which of the instances of `Config` will have its `optIn` property set to `false`?

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:

typescript
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.

typescript
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...

typescript
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...

typescript
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}
Multiple Choice

What is the output of the function mystery() defined above?

Select One

[Playground]

popularity
On Fire
posted
May. 04, 2024