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:
- Using the non-null assertion operator
!
- Implementing an inline null check
- 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 toHTMLElement
NonNullable<HTMLElement | null>
evaluates toHTMLElement
NonNullable<HTMLElement | null | undefined>
evaluates toHTMLElement
NonNullable<null>
evaluates tonever
NonNullable<undefined>
evaluates tonever
NonNullable<null | undefined>
evaluates tonever
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 article and 44 others are part of the TypeScript Evolution series. Have a look!