Overview
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”:
|
|
We’re calling the root.addEventListener()
method to attach a click handler to the element. However, TypeScript reports a type error:
|
|
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:
|
|
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:
|
|
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:
|
|
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):
|
|
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
:
|
|
We’re using the unknown
type for the value
parameter here to allow call sites 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:
|
|
However, TypeScript still produces a type error for the root.addEventListener()
method call:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
As a result of this type narrowing, our example now type-checks correctly:
|
|
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.