DEV Community

Cover image for Assertion functions in TypeScript
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Assertion functions in TypeScript

Written by Matteo Di Pirro✏️

Assertion functions in TypeScript are a very expressive type of function whose signature states that a given condition is verified if the function itself returns.

In its basic form, a typical assert function just checks a given predicate and throws an error if such a predicate is false. For example, Node.js’s assert throws an AssertionError if the predicate is false.

TypeScript, since its version 3.7, has gone a little beyond that by implementing the support of assertions at the type system level.

In this article, we’re going to explore assertion functions in TypeScript and see how they can be used to express invariants on our variables.

Table of Contents

JavaScript-like assertions

Node.js comes with a predefined assert function. As we mentioned in the introduction, it throws an AssertionError if a given predicate is false:

const aValue = 10
assert(aValue === 20)
Enter fullscreen mode Exit fullscreen mode

In JavaScript, this was useful to guard against improper types in a function:

function sumNumbers(x, y) {
  assert(typeof x === "number");
  assert(typeof y === "number");
  return x + y;
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately, the code flow analysis does not take into account those assertions. In fact, they are simply evaluated at runtime and then forgotten.

With its assertion functions, TypeScript’s code flow analysis will be able to use the type of a function (in brief, its signature) to infer some properties of our code. We can use this new feature to make guarantees of our types throughout our code.

TypeScript-like assertion

An assertion function specifies, in its signature, the type predicate to evaluate. For instance, the following function ensures a given value be a string:

function isString(value: unknown): asserts value is string {
  if (typeof value !== "string") throw new Error("Not a string")
}
Enter fullscreen mode Exit fullscreen mode

If we invoke the function above with a given parameter, and it returns correctly, TypeScript knows that value has type string. Hence, it will narrow down its type to string:

const aValue: string|number = "Hello"
isString(aValue)
// The type of aValue is narrowed to string here
Enter fullscreen mode Exit fullscreen mode

Of course, nothing prevents us from messing up the assertion. For example, we could have written a (wrong) function as follows:

function isString(value: unknown): asserts value is string {
  if (typeof value !== "number") throw new Error("Not a string")
}
Enter fullscreen mode Exit fullscreen mode

Note that we’re now checking whether value's type is not number, instead of string. In this case, TypeScript’s code flow analysis will see a Value of type never, instead of string as above.

Assertion functions can be very useful with enums:

type AccessLevel = "r" | "w" | "rw"

const writeOnly = "w"

function allowsReadAccess(level: AccessLevel): asserts level is "r" | "rw" {
    if (!level.includes("r")) throw new Error("Read not allowed")
}

allowsReadAccess(writeOnly)
Enter fullscreen mode Exit fullscreen mode

In the example above, we first defined a type whose value can only be either "r", "w", or "rw". Let’s assume such a type simply defines the three types of access to a given resource. We then declare an assertion function throwing if its actual parameter does not allow a read operation.

As you can see, we’re narrowing down the type explicitly, stating that, if the function returns, the value must be either "r" or "rw". If we call allowsReadAccess with writeOnly as the actual parameter, we'll get an error as expected, stating that "Read access is not allowed".

Another common use of assertion functions is expressing non-nullability. The following snippet of code shows a way to make sure a value is defined, that is it’s not either null or undefined:

function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value === undefined || value === null) {
    throw new Error(`${value} is not defined`)
  }
}
Enter fullscreen mode Exit fullscreen mode

Where NonNullable<T> is a TypeScript type that excludes null and undefined from the legit values of the type T.

Function declarations and expressions

At the time of writing, assertion functions may not be defined as plain function expressions. Generally speaking, function expressions can be seen as anonymous functions; that is, functions without a name:

// Function declaration
function fun() { ... }

// Function expression
const fun = function() { ... }
Enter fullscreen mode Exit fullscreen mode

The main advantage of function declarations is hoisting, which is the possibility of using the function anywhere in the file where it’s defined. On the other hand, function expressions can only be used after they are created.

There is actually a workaround to write assertion functions as function expressions. Instead of defining the function along with its implementation, we’ll have to define its signature as an isolated type:

// Wrong
// Error: A type predicate is only allowed in return type position for functions and methods.
// Error: Type '(value: any) => void' is not assignable to type 'void'.
const assertIsNumber: asserts value is number = (value) => {
  if (typeof value !== 'number') throw Error('Not a number')
}

// Correct
type AssertIsNumber = (value: unknown) => asserts value is number
const assertIsNumber: AssertIsNumber = (value) => {
  if (typeof value !== 'number') throw Error('Not a number')
}
Enter fullscreen mode Exit fullscreen mode

Assertion functions and type guards

Assertion functions in TypeScript are somewhat similar to type guards. Type guards were originally introduced to perform runtime checks to guarantee the type of a value in a given scope.

In particular, a type guard is a function that simply evaluates a type predicate, returning either true or false. This is slightly different from assertion functions, which, as we saw above, are supposed to throw an error instead of returning false if the predicate is not verified.

function isString(value: unknown): value is string {
    return typeof value === "string"
}

// Type guards can also be declared as function expression
const isStringExp = (value: unknown): value is string =>
    typeof value === "string"
Enter fullscreen mode Exit fullscreen mode

There is another big difference though. Assertion functions can also be used without a type predicate, as we’ll see in the following section.

Assertion functions without a type predicate

The assertion functions we’ve seen so far were all checking whether a given value had a given type. Hence, they were all fairly tailored for the target type. Nonetheless, assertion functions give us much more power. In particular, we can write a completely general function asserting a condition that gets input as a parameter:

function assert(condition: unknown, msg?: string): asserts condition {
  if (condition === false) throw new Error(msg)
}
Enter fullscreen mode Exit fullscreen mode

The assert function now inputs a condition, whose type is unknown, and, possibly, a message. Its body simply evaluates such a condition. If it is false, then assert throws an error, as expected.

Note, however, that the signature makes use of the condition parameter after asserts. This way, we’re telling TypeScript code flow analysis that, if the function returns correctly, it can assume that whatever predicate we passed in was, in fact, verified.

TypeScript’s Playground gives us a pretty good visual representation of what the code flow analysis does. Let’s consider the following snippet of code, where we generate a random number and then call assert to make sure the generated number is 10:

const randomNumber = Math.random()
assert(randomNumber == 10, "The number must be equal to 10")
randomNumber
Enter fullscreen mode Exit fullscreen mode

If we inspect the inferred properties of randomValue before the call to assert, TypeScript just tells us the type (Figure 1). RandomNumber Figure 1. TypeScript is only able to infer the type of randomNumber before the call to assert. Then, as soon as we call assert, with the condition randomNumber == 10, TypeScript knows that the value will be 10 for the rest of the execution (Figure 2). RandomNumber set to 10 Figure 2. TypeScript now tells us that randomNumber is set to 10.

Lastly, if we attempt to check the equality of randomNumber and another number, TypeScript will be able to evaluate the property without even running the program. For example, the code flow analysis will complain about the following assignment, saying, “This condition will always return 'false' since the types '10' and '20' have no overlap.”:

const pred = (randomNumber === 20)
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we dove into what TypeScript assertion functions are and how we can use them to have the code flow analysis infer a set of properties about our values. They are a very nice feature that makes sense considering that TypeScript is transpiled to JavaScript, which gives programmers a lot more flexibility.

In particular, we took a look at a handful of usages, including narrowing types down and expressing conditions on the actual value of our variables. Lastly, we briefly mentioned the differences and similarities with type guards and grasped the syntactic limitations of assertions functions.


LogRocket: Full visibility into your web and mobile apps

LogRocket signup

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

Try it for free.

Top comments (0)