Marius Schulz
Marius Schulz
Front End Engineer

Assertion Functions in TypeScript

TypeScript 3.7 implemented support for assertion functions in the type system. An assertion function is a function that throws an error if something unexpected happened. Using assertion signatures, we can tell TypeScript that a function should be treated as an assertion function.

#An Example: The document.getElementById() Method

Let's start by looking at an example in which we're using the document.getElementById() method to find a DOM element that has the ID "root":

const root = document.getElementById("root");

root.addEventListener("click", e => {
  /* ... */
});

We're calling the root.addEventListener() method to attach a click handler to the element. However, TypeScript reports a type error:

const root = document.getElementById("root");

// Object is possibly null
root.addEventListener("click", e => {
  /* ... */
});

The root variable is of type HTMLElement | null, which is why TypeScript reports a type error "Object is possibly null" when we're trying to call the root.addEventListener() method. In order for our code to be considered type-correct, we somehow need to make sure that the root variable is non-null and non-undefined before calling the root.addEventListener() method. We have a couple of options for how we can do that, including:

  1. Using the non-null assertion operator !
  2. Implementing an inline null check
  3. Implementing an assertion function

Let's look at each of the three options.

#Using the Non-Null Assertion Operator

First up, we'll try and use the non-null assertion operator ! which is written as a post-fix operator after the document.getElementById() call:

const root = document.getElementById("root")!;

root.addEventListener("click", e => {
  /* ... */
});

The non-null assertion operator ! tells TypeScript to assume that the value returned by document.getElementById() is non-null and non-undefined (also known as “non-nullish”). TypeScript will exclude the types null and undefined from the type of the expression to which we apply the ! operator.

In this case, the return type of the document.getElementById() method is HTMLElement | null, so if we apply the ! operator, we get HTMLElement as the resulting type. Consequently, TypeScript no longer reports the type error that we saw previously.

However, using the non-null assertion operator is probably not the right fix in this situation. The ! operator is completely erased when our TypeScript code is compiled to JavaScript:

const root = document.getElementById("root");

root.addEventListener("click", e => {
  /* ... */
});

The non-null assertion operator has no runtime manifestation whatsoever. That is, the TypeScript compiler does not emit any validation code to verify that the expression is actually non-nullish. Therefore, if the document.getElementById() call returns null because no matching element can be found, our root variable will hold the value null and our attempt to call the root.addEventListener() method will fail.

#Implementing an Inline Null Check

Let's now consider the second option and implement an inline null check to verify that the root variable holds a non-null value:

const root = document.getElementById("root");

if (root === null) {
  throw Error("Unable to find DOM element #root");
}

root.addEventListener("click", e => {
  /* ... */
});

Because of our null check, TypeScript's type checker will narrow the type of the root variable from HTMLElement | null (before the null check) to HTMLElement (after the null check):

const root = document.getElementById("root");

// Type: HTMLElement | null
root;

if (root === null) {
  throw Error("Unable to find DOM element #root");
}

// Type: HTMLElement
root;

root.addEventListener("click", e => {
  /* ... */
});

This approach is much safer than the previous approach using the non-null assertion operator. We're explicitly handling the case in which the root variable holds the value null by throwing an error with a descriptive error message.

Also, note that this approach does not contain any TypeScript-specific syntax whatsoever; all of the above is syntactically valid JavaScript. TypeScript's control flow analysis understands the effect of our null check and narrows the type of the root variable in different places of the program — no explicit type annotations needed.

#Implementing an Assertion Function

Lastly, let's now see how we can use an assertion function to implement this null check in a reusable way. We'll start by implementing an assertNonNullish function that will throw an error if the provided value is either null or undefined:

function assertNonNullish(
  value: unknown,
  message: string
) {
  if (value === null || value === undefined) {
    throw Error(message);
  }
}

We're using the unknown type for the value parameter here to allow callsites to pass a value of an arbitrary type. We're only comparing the value parameter to the values null and undefined, so we don't need to require the value parameter to have a more specific type.

Here's how we would use the assertNonNullish function in our example from before. We're passing it the root variable as well as the error message:

const root = document.getElementById("root");
assertNonNullish(root, "Unable to find DOM element #root");

root.addEventListener("click", e => {
  /* ... */
});

However, TypeScript still produces a type error for the root.addEventListener() method call:

const root = document.getElementById("root");
assertNonNullish(root, "Unable to find DOM element #root");

// Object is possibly null
root.addEventListener("click", e => {
  /* ... */
});

If we have a look at the type of the root variable before and after the assertNonNullish() call, we'll see that it is of type HTMLElement | null in both places:

const root = document.getElementById("root");

// Type: HTMLElement | null
root;

assertNonNullish(root, "Unable to find DOM element #root");

// Type: HTMLElement | null
root;

root.addEventListener("click", e => {
  /* ... */
});

This is because TypeScript doesn't understand that our assertNonNullish function will throw an error if the provided value is nullish. We need to explicitly let TypeScript know that the assertNonNullish function should be treated as an assertion function that asserts that the value is non-nullish, and that it will throw an error otherwise. We can do that using the asserts keyword in the return type annotation:

function assertNonNullish<TValue>(
  value: TValue,
  message: string
): asserts value is NonNullable<TValue> {
  if (value === null || value === undefined) {
    throw Error(message);
  }
}

First of all, note that the assertNonNullish function is now a generic function. It declares a single type parameter TValue that we use as the type of the value parameter; we're also using the TValue type in the return type annotation.

The asserts value is NonNullable<TValue> return type annotation is what's called an assertion signature. This assertion signature says that if the function returns normally (that is, if it doesn't throw an error), it has asserted that the value parameter is of type NonNullable<TValue>. TypeScript uses this piece of information to narrow the type of the expression that we passed to the value parameter.

The NonNullable<T> type is a conditional type that is defined in the lib.es5.d.ts type declaration file that ships with the TypeScript compiler:

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;

When applied to the type T, the NonNullable<T> helper type removes the types null and undefined from T. Here are a few examples:

  • NonNullable<HTMLElement> evaluates to HTMLElement
  • NonNullable<HTMLElement | null> evaluates to HTMLElement
  • NonNullable<HTMLElement | null | undefined> evaluates to HTMLElement
  • NonNullable<null> evaluates to never
  • NonNullable<undefined> evaluates to never
  • NonNullable<null | undefined> evaluates to never

With our assertion signature in place, TypeScript now correctly narrows the type of the root variable after the assertNonNullish() function call. The type checker understands that when root holds a nullish value, the assertNonNullish function will throw an error. If the control flow of the program makes it past the assertNonNullish() function call, the root variable must contain a non-nullish value, and therefore TypeScript narrows its type accordingly:

const root = document.getElementById("root");

// Type: HTMLElement | null
root;

assertNonNullish(root, "Unable to find DOM element #root");

// Type: HTMLElement
root;

root.addEventListener("click", e => {
  /* ... */
});

As a result of this type narrowing, our example now type-checks correctly:

const root = document.getElementById("root");
assertNonNullish(root, "Unable to find DOM element #root");

root.addEventListener("click", e => {
  /* ... */
});

So here we have it: a reusable assertNonNullish assertion function that we can use to verify that an expression has a non-nullish value and to narrow the type of that expression accordingly by removing the null and undefined types from it.

This post is part of the TypeScript Evolution series.