loading...

Optional Chaining (?.), Nullish Coalescing (??), and Non-Null Assertion Operators (!) in TypeScript

jamenamcinteer profile image Jamena McInteer ・5 min read

If you've worked with JavaScript you've probably become all to familiar with the following error: TypeError: Cannot read property 'value' of undefined where value is a property on an object that you are trying to access. For example, if you are expecting to receive an object from an API, you might have something in your code like this:

const cat = response.data.animals.cat;

But if the API didn't return any animal object, then this code would throw the error TypeError: Cannot read property 'cat' of undefined. It's even possible that your response object doesn't even have the data object, and you end up with an error like TypeError: Cannot read property 'animals' of undefined.

To avoid these errors, we usually write something like this:

let cat;
if (response.data && response.data.animals) {
  cat = response.data.animals.cat;
}

But now, not only is this code more verbose, cat is no longer a constant (since using const cat within the if statement would make it unusable outside of the if statement).

You might also try using ternary operators to make your code a bit less verbose and keep the const, like this:

const cat = response.data && response.data.animals ? response.data.animals.cat : undefined;

Optional Chaining Operator

This is where optional chaining (the Optional Chaining operator) comes in for JavaScript (currently in stage 4 and has recently arrived in Chrome) and TypeScript (since version 3.7, November 6, 2019). Now we can achieve what the previous two snippets of code did with less verbosity:

const cat = response.data?.animals?.cat;

In this case, cat will be undefined if response.data is undefined or if response.data.animals is undefined, instead of throwing an error. This looks a lot more like the first snippet of code we attempted to use.

To further understand how this works, here is what the TC39 proposal states:

If the operand at the left-hand side of the ?. operator evaluates to undefined or null, the expression evaluates to undefined. Otherwise, the targeted property access, method or function call is triggered normally.

Nullish Coalescing Operator

What if we don't want cat to be undefined if response.data or response.data.animals is undefined? Let's say we want cat to be a string, "No cat could be found.".

Normally, we might do something like this:

let cat;
if (response.data && response.data.animals) {
  cat = response.data.animals.cat;
}
else {
  cat = "No cat could be found.";
}

Or, using the ternary operator:

const cat = response.data && response.data.animals ? response.data.animals.cat : "No cat could be found.";

There is another operator, also in stage 4 for JavaScript and currently available in TypeScript, called the Nullish coalescing operator that can be used together with optional chaining to make our lives a little easier:

const cat = response.data?.animals?.cat ?? "No cat could be found.";

If you are not used to this syntax it may take a little getting used to, but should help make your code less verbose.

Again, a snippet from the TC39 proposal that may help understand how this operator works:

If the expression as the left-hand side of the ?? operator evaluates to undefined or null, its right-hand side is returned.

This operator is also useful when working with falsy values that are not null or undefined. For example, the following will print Hello since the string is empty and therefore falsy, even if it is not null or undefined:

const someEmptyValue = '';
console.log(someEmptyValue || 'Hello');

The same is true for the following, since 0 is also falsy:

const someZeroValue = 0;
console.log(someZeroValue || 'Hello');

And for the following, since false is also falsy:

const someFalseValue = false;
console.log(someFalseValue || 'Hello');

The results of these examples may be what we intend, depending on the application, but there are cases where we only want them to be falsy if they are null or undefined (called nullary values) and not the other cases.

For example, the following will print an empty string, 0, and false respectively, and not "Hello":

const someEmptyValue = '';
console.log(someEmptyValue ?? 'Hello');

const someZeroValue = 0;
console.log(someZeroValue ?? 'Hello');

const someFalseValue = false;
console.log(someFalseValue ?? 'Hello');

This nullish coalescing operator can be super useful if you're not sure if an optional argument for a function is being passed and you want to use different logic depending on whether or not it is. Consider this TypeScript snippet:

let inputValue: string = 'Jane Doe';
const validateInput = (value?: string | undefined) => {
  const checkValue: string = value ?? inputValue;
  if(!checkValue) {// will check for an empty string in this case
    return false;
  }
  return true;
}

With this snippet, we could pass a value to the function, for example validateInput(someNewValue) and the validation will occur on the value passed. However, if no value is passed as in validateInput() then the validation will occur on inputValue (this may be a state variable that is updated when the input is changed, for example).

Non-Null Assertion Operator

There may be times when you are writing TypeScript that you put in checks to ensure that a variable is not going to be null or undefined later in the code, but the compiler throws an Object is possibly 'null' or Object is possibly 'undefined' error, or something like undefined is not assignable to number. For example:

const addNumbers = (a: number | undefined, b: number) => {
  const c: number = a;// throws an error
  const d: number = b;
  return c + d;
}

Normally, this is a good thing, but imagine we had a scenario where we know that a and b are numbers whenever this function is called, even though they could be undefined at some other point in the program. Maybe you have some state in a React app that is initially undefined, but at the point that a function like this is called they are always already set. You can use the non-null assertion operator for these cases. This works if you have the --strickNullChecks flag turned on and are writing TypeScript (not JavaScript).

const addNumbers = (a: number | undefined, b: number) => {
  const c: number = a!;// no error
  const d: number = b;
  return c + d;
}

Be careful when using this operator though! If a turns out to be undefined anyway, no error will be thrown for c and c will end up undefined even though it should not have that type. Essentially, the code will fall back to JavaScript instead of using the type checks that TypeScript provides. This is a type annotation that does not change the compiled code. It is usually better to use null checks when you can (like using the previous two operators). You can read more about this operator in this article: Cleaner TypeScript With the Non-Null Assertion Operator.

Conclusion

I hope this article is useful for anyone learning TypeScript who hasn't learned about these operators yet. Feel free to leave a comment if you need clarification or if you find a typo or inaccuracy. 💕

Discussion

markdown guide