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
nullmeans: 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.
tsconst userInfo = {name: "Mike",email: "mike@example.com",secondaryEmail: null, // user has no secondary email}
undefined
undefinedmeans 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:
tsinterface FormInProgress {createdAt: Datedata: FormDatacompletedAt?: Date}const formInProgress: FormInProgress = {createdAt: new Date(),data: new FormData(),}function submitForm() {formInProgress.completedAt = new Date()}
void
We have already covered this in the functions chapter, but as a reminder:
voidshould exclusively be used to describe that a function’s return value should be ignored
tsTryconsole .log (`console.log returns nothing.`)
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:
tsTrytypeGroceryCart = {fruits ?: {name : string;qty : number }[]vegetables ?: {name : string;qty : number }[]}constcart :GroceryCart = {}'cart.fruits' is possibly 'undefined'.18048'cart.fruits' is possibly 'undefined'.. cart .fruits push ({name : "kumkuat",qty : 1 })cart .fruits !.push ({name : "kumkuat",qty : 1 })
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
Definite assignment assertion
The definite assignment !: assertion 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:
tsTryclassThingWithAsyncSetup {setupPromise :Promise <any>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.: boolean isSetup constructor() {this.setupPromise = newPromise ((resolve ) => {this.isSetup = falsereturn this.doSetup (resolve )}).then (() => {this.isSetup = true})}private asyncdoSetup (resolve : (value : unknown) => void) {// some async stuff}}
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
tsTryletmyThing = newThingWithAsyncSetup ()myThing .isSetup // what if this isn't assigned yet?
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.
tsTryclassThingWithAsyncSetup {setupPromise :Promise <any> // ignore the <any> for nowisSetup !: booleanconstructor() {this.setupPromise = newPromise ((resolve ) => {this.isSetup = falsereturn this.doSetup ()}).then (() => {this.isSetup = true})}private asyncdoSetup () { }}
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.
Optional chaining ?.
A less hazardous tool, relative to the non-null assertion operator is optional chaining.
Let’s say we have a big JSON object with a structure like this
typescripttype Payment = {id: stringamount: numbercreatedAt: Date}type Invoice = {id: stringdue: numberpayments: Payment[]lastPayment?: PaymentcreatedAt: Date}type Customer = {id: string,lastInvoice?: Invoiceinvoices: Invoice[]};type ResponseData = {customers?: Customer[]customer?: Customer}
So, we can have one or many Customers, each of which may have one or more Invoices, each of which may have one or more Payments.
Now let’s say we want to render information on a dashboard, for the customer’s most recent payment on any invoice (or leave blank if they haven’t made any payments).
There’s a whole lot of presence checking we’d need to perform!
tsTryfunctiongetLastPayment (data :ResponseData ): number | undefined {const {customer } =data ;if (!customer ) return;const {lastInvoice } =customer ;if (!lastInvoice ) return;const {lastPayment } =lastInvoice ;if (!lastPayment ) return;returnlastPayment .amount ;}
All this, just to sort of drill down and find something if it’s there. Optional chaining gives us a more concise way to do this
tsTryfunctiongetLastPayment (data :ResponseData ): number | undefined {returndata ?.customer ?.lastInvoice ?.lastPayment ?.amount }
Behind the scenes, what’s happening here is very similar to the more lengthy version of this function that we wrote above. Here’s the compiled output (target: ES2017)
tsTryfunction getLastPayment(data) {var _a, _b, _c;return (_c = (_b = (_a = data === null || data === void 0 ? void 0 : data.customer) === null || _a === void 0 ? void 0 : _a.lastInvoice) === null || _b === void 0 ? void 0 : _b.lastPayment) === null || _c === void 0 ? void 0 : _c.amount;}
If any step of our “chain” ends up being undefined, the whole expression ends up evaluating to undefined
Nullish coalescing ??
Similar to the optional chaining operator, nullish coalescing allows for succinct handling of the possibility that something might be undesirably null or undefined.
Let’s imagine a scenario where we’re building a video player, and have the following requirements
- The range of allowed volume is
(0 - 100)in increments of 25, where0indicates"mute" - Totally new users should start with a default volume of
50 - When users adjust their volume, we save it in a
configobject (imagine this is persisted somewhere) and restore their previous volume when they leave and come back
tsTrytypePlayerConfig = {volume ?: 0 | 25 | 50 | 75 | 100}functioninitializePlayer (config :PlayerConfig ): void {constvol = typeofconfig .volume === 'undefined' ? 50 :config .volume setVolume (vol );}
This line is where the interesting stuff is happening, and readability is not great
tsTryconstvol = typeofconfig .volume === 'undefined' ? 50 :config .volume
At first glance, we might want to try the logical OR operator || since that will handle the undefined case
tsTryconstvol =config .volume || 50
Oops! This is more readable, but our “mute” value 0 has disappeared. Thankfully, we can do the same thing with our nullish coalescing operator ??, which does not perform a truthy/falsy check, but a specific check for null and undefined, and we’ll get the right result
tsTryconstvol =config .volume ?? 50
-
Where “proven” means, “the compiler can’t convince itself.”
↩