Learn TypeScript w/ Mike North

Nullish values

June 08, 2021

Table of Contents

There are situations where we have to plan for, and deal with the possibility that values are null or undefined. In this chapter we’ll dive deep into null, undefined, definite assignment, non-nullish coalescing, optional chaining and the non-null assertion operator.

Although null, void and undefined are all used to describe “nothing” or “empty”, they are independent types in TypeScript. Learning to use them to your advantage, and they can be powerful tools for clearly expressing your intent as a code author.

null

null means: there is a value, and that value is nothing. While some people believe that null is not an important part of the JS language, I find that it’s useful to express the concept of a “nothing” result (kind of like an empty array, but not an array).

This nothing is very much a defined value, and is certainly a presence — not an absence — of information.

ts
const userInfo = {
name: "Mike",
email: "mike@example.com",
secondaryEmail: null, // user has no secondary email
}

undefined

undefined means the value isn’t available (yet?)

In the example below, completedAt will be set at some point but there’s a period of time when we haven’t yet set it. undefined is an unambiguous indication that there may be something different there in the future:

ts
const formInProgress = {
createdAt: new Date(),
data: new FormData(),
completedAt: undefined, //
}
function submitForm() {
formInProgress.completedAt = new Date()
}

void

We have already covered this in the functions chapter, but as a reminder:

void should exclusively be used to describe that a function’s return value should be ignored

ts
console.log(`console.log returns nothing.`)
(method) Console.log(...data: any[]): void
Try

Non-null assertion operator

The non-null assertion operator (!.) is used to cast away the possibility that a value might be null or undefined.

Keep in mind that the value could still be null or undefined, this operator just tells TypeScript to ignore that possibility.

If the value does turn out to be missing, you will get the familiar cannot call foo on undefined family of errors at runtime:

ts
type GroceryCart = {
fruits?: { name: string; qty: number }[]
vegetables?: { name: string; qty: number }[]
}
 
const cart: GroceryCart = {}
 
cart.fruits.push({ name: "kumkuat", qty: 1 })
'cart.fruits' is possibly 'undefined'.18048'cart.fruits' is possibly 'undefined'.
(property) fruits?: { name: string; qty: number; }[] | undefined
cart.fruits!.push({ name: "kumkuat", qty: 1 })
Try

I recommend against using this in your app or library code, but if your test infrastructure represents a throw as a test failure (most should) this is a great type guard to use in your test suite.

In the above situation, if fruits was expected to be present and it’s not, that’s a very reasonable test failure emoji-tada

Definite assignment operator

The definite assignment !: operator is used to suppress TypeScript’s objections about a class field being used, when it can’t be proven1 that it was initialized.

Let’s look at the following example:

ts
class ThingWithAsyncSetup {
setupPromise: Promise<any> // ignore the <any> for now
isSetup: boolean
Property 'isSetup' has no initializer and is not definitely assigned in the constructor.2564Property 'isSetup' has no initializer and is not definitely assigned in the constructor.
 
constructor() {
this.setupPromise = new Promise((resolve) => {
this.isSetup = false
return this.doSetup(resolve)
}).then(() => {
this.isSetup = true
})
}
 
private async doSetup(resolve: (value: unknown) => void) {
// some async stuff
}
}
Try

TypeScript is warning me that someone could create an instance of this class and immediately attempt to access .isSetup before it gets a boolean value

ts
let myThing = new ThingWithAsyncSetup()
myThing.isSetup // what if this isn't assigned yet?
(property) ThingWithAsyncSetup.isSetup: boolean
Try

What I know (that the compiler doesn’t) is that the function passed into the Promise constructor is invoked synchronously, meaning by the time we receive our instance of ThingWithAsyncSetup, the isSetup property will most certainly have a value of false.

This is a good example of a totally appropriate use of the definite assignment operator, where I as the code author have some extra context that the compiler does not.


  1. Where “proven” means, “the compiler can’t convince itself.”



© 2023 All Rights Reserved