Engineering

TypeScript 3.7 Features in Production: Optional Chaining, Nullish Coalescing, and Assertion Functions

Jake Marsh
October 07, 2019

At Monolist, we're building the command center for engineers. We integrate with all of the tools engineers use (code hosting, project management, alerting), and aggregate all of their tasks in one place. If you've read our previous posts, you know we're big fans of TypeScript and highly recommend it.

Microsoft has just announced the TypeScript 3.7 Beta, and it included multiple features we were excited to start using ASAP here at Monolist. We're going to be diving into some of these features and how we're already using them in production.

Optional Chaining

Optional chaining is a new feature that allows you to chain property accesses without worrying about null or undefined. If it encounters one of these values, it will stop executing the expression. This means you no longer have to chain && when accessing a nullable property.

If you're familiar with Ruby (which we use for our API), this is similar to the safe navigation operator.

In Practice

There are a few patterns that emerge when writing a React app. Optional props mean you're often doing null checks and && chaining to ensure a prop exists before accessing it.

For some of our reusable components, we have optional render props to render any context-specific supplementary content. This ends up looking something like this:

<div>
  {props.renderSupplementaryContent && props.renderSupplementaryContent()}
</div>

With optional chaining, this becomes:

<div>
  {props.renderSupplementaryContent?.()}
</div>

Not bad! A similar pattern can occur when trying to access properties of an optional prop. Here's a snippet of our code for rendering pull request approvals, in which props.update may not exist:

function getOverlay(): React.ReactNode {
  return (props.update && props.update.text) || `Approved by ${props.approvedBy.join(', ')}`;
}

With optional chaining, this becomes:

function getOverlay(): React.ReactNode {
  return props.update?.text || `Approved by ${props.approvedBy.join(', ')}`;
}

These are just a few examples in a React app that demonstrate how this new feature will be helpful. Although simple, it removes a lot of boilerplate and helps readability.

Nullish Coalescing

Although the name sounds a little intimidating, this feature is simple: the new ?? operator provides a way to fall back to a default value in a more reliable manner than ||.

Since || triggers implicit type coercion, any falsy value at the beginning will be passed over for the following value. With the new ?? operator, it will only fall back to the subsequent value if the first value is truly null or undefined.

In Practice

We recently added full diff browsing and commenting support to Monolist.

One obvious requirement of this feature is the ability to map comment threads back to their original line in the git diffs. When doing this, we're often doing comparisons on the relevant line numbers. Here's an example utility function we use:

function getLineNumberForChange(change: IChange): number {
  return change.newLineNumber || change.oldLineNumber;
}

This means that whenever we pass in a change (a single line of a git diff), we return either its newest line number, or if that doesn't exist then fall back to its old line number. For now this works because our line number indices start at 1. However, if newLineNumber were ever 0, we'd pass right over it and erroneously return oldLineNumber. We can now fix this easily with nullish coalescing:

function getLineNumberForChange(change: IChange): number {
  return change.newLineNumber ?? change.oldLineNumber;
}

This will only skip over newLineNumber if it's explicitly null or undefined! No more skipping over 0.

Assertion Functions

The last "headline" feature in TypeScript 3.7 that we'll go over is assertion functions. These ensure that whatever condition is being checked must be true for the remainder of the containing scope. These assertion functions can take two forms.

The first, asserts condition, says that whatever gets passed as the condition must be true if the assert returns. Otherwise, an error is thrown. This is similar to Node's assert module.

The second, asserts val is <type>, doesn't check for a condition but rather that a specific variable or property has a different type. These are Similar to type predicates.

In Practice

Since Monolist integrates with many different applications and displays them all in one similar format, we have many different item types that contribute to one union type: ActionItem. This means there are many places where we have to check the type of the item before we proceed with integration-specific logic.

Here's an example:

function getActionsForGithubPullRequestActionItem(actionItem: ActionItem): Action[ {
  const possibleActions: Action[] = [];

  if (actionItem.actionItemType !== 'githubPullRequest') {
    return [];
  }

  const _actionItem = actionItem as GithubPullRequestActionItem;

  if (_actionItem.state === 'open') {
    if (_actionItem.githubPullRequest.canBeApproved) {
      possibleActions.push('approve');
    }

    possibleActions.push('merge');
  }

  return possibleActions;
}

Here, we're getting the available actions that a user can take on their GitHub pull request items. However, we first have to ensure that the item is the type we expect: a githubPullRequest. This requires first checking the type of the item, and then re-aliasing it as the proper type so that our later property accesses don't throw (like actionItem.githubPullRequest.canBeApproved).

Using the second assertion function signature, we can create an assertion function to be re-used in places like this moving forward:

function assertIsGithubPullRequestItem(val: ActionItem): asserts val is GithubPullRequestActionItem {
  if actionItem.actionItemType !== 'githubPullRequest') {
    throw new AssertionError('Not a GitHub pull request item!');
  }
}

function getActionsForGithubPullRequestActionItem(actionItem: ActionItem): Action[] {
  assertIsGithubPullRequestItem(actionItem);

  const possibleActions: Action[] = [];

  if (actionItem.state === 'open') {
    if (actionItem.githubPullRequest.canBeApproved) {
      possibleActions.push('approve');
    }

    possibleActions.push('merge');
  }

  return possibleActions;
}

Now, assuming our newly added assertion function doesn't throw, the rest of getActionsForGithubPullRequestActionItem will know that actionItem is a GithubPullRequestActionItem. Again, this is similar to what can be achieved with type predicates.

Wrapping Up

These are just a few of the new features being added to TypeScript regularly. Read their full announcement here, and subscribe to our mailing list to keep up to date on any of our future posts.

❗️ Are you a software engineer?

At Monolist, we're building software to help engineers be their most productive. If you want to try it for free, just click here.

Follow us on Twitter