HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

31 Assertion functions

In programming, an assertion function is a function that checks if a condition is true at a particular point in a program and throws an exception if it isn’t. In this chapter, we explore:

31.1 Assertion functions in Node.js

Node.js has a whole module, 'node:assert/strict' with assertion functions. You can check out its documentation online. Let’s look at two important examples.

31.1.1 assert(cond) for asserting conditions

Function assert() lets us assert conditions. I see it as a comment that is written in code and therefore verifiable – e.g.:

import * as path from 'node:path';
import assert from 'node:assert/strict';
export function replaceExt(filePath: string, ext: string): string {
  assert(ext.startsWith('.')); // (A)
  const parsed = path.parse(filePath);
  return path.join(parsed.dir, parsed.name + ext);
}

ext having to start with a dot could be a comment. Instead, we chose to write it as an assertion (line A). If the condition is false, assert() throws an exception. In other words: assert(cond) means “cond must be true”.

31.1.2 assert.equal(value1, value2) for asserting equality

The assertion function assert.equal() is mainly used for testing:

import assert from 'node:assert/strict';
const add = (x: number, y: number) => x + y;
assert.equal(
  add(3, 4), 7
);

If the two arguments of assert.equal() are not equal, it throws an exception.

31.2 TypeScript’s support for assertion functions

TypeScript provides special support for assertion functions. There are two kinds of assertion signatures we can specify as the return types of void functions.

First, we can assert a boolean condition (think assert()):

function assertCondition(condition: boolean): asserts condition {
  // ...
}

Second, we can assert the type of an argument:

function assertArgIsMyType(arg: unknown): asserts arg is MyType {
  // ...
}

Next, we’ll examine how calling an assertion function affects the type level.

31.3 Asserting a boolean condition: asserts «cond»

This is a re-implementation of Node’s assert() function so that we can also use it in browsers. It asserts a boolean condition:

function assertTrue(
  condition: boolean, message?: string | Error
): asserts condition { // (A)
  if (!condition) {
    if (message instanceof Error) {
      throw message;
    }
    message ??= 'Assertion failed';
    throw new Error(message); // (B)
  }
}

The assertion signature in line A makes assertTrue an assertion function. We’ll explore the type-level consequences of that in the next subsection. The following design decisions were made:

31.3.1 Asserting type guards

Using a type guard (a boolean expression exploring the types of values) in a condition narrows types:

if («typeGuard») {
  // One or more types are narrowed
}

Asserting a type guard has a similar effect:

assertTrue(«typeGuard»);
// One or more types are narrowed

Let’s look at an example:

function func(value: unknown) {
  assertTrue(value instanceof Set); // (A)
  assertType<Set<any>>(value);
}

value instanceof Set is a type guard and because we asserted it in line A, we narrowed the type of value to Set<any>.

31.4 Asserting the type of an argument: asserts «arg» is «type»

The following assertion function narrows the type of its parameter value:

function assertIsNumber(value: unknown): asserts value is number {
  if (typeof value !== 'number') {
    throw new TypeError();
  }
}

Let’s use assertIsNumber():

function func(value: unknown) {
  assertIsNumber(value); // (A)
  assertType<number>(value);
}

The assertion in line A narrows the type of value from unknown to number.

Note that assertIsNumber() really narrows:

function func(str: string) {
  assertIsNumber(str);
  // string & number is `never`
  assertType<string & number>(str);
}

We narrowed the original type of str, string to the narrower type string & number – which is the same as the bottom type never.

31.4.0.1 Example assertion function: assert.equal()

This is a simplified version of Node’s assert.equal() function that has the same types:

function assertEqual<T>(
  actual: unknown, expected: T, message?: string | Error
): asserts actual is T {
  if (actual !== expected) {
    if (message instanceof Error) {
      throw message;
    }
    message ??= `Assertion failed: ${actual} === ${expected}`;
    throw new Error(message);
  }
}

If it terminates, the type of actual is narrowed to the type T. That’s rarely useful, but I found it interesting that the function does that.

31.4.0.2 Example assertion function: assertNonNullable()

The following assertion function asserts that its parameter value is not nullable:

export function assertNonNullable<T>(
  value: T, message?: string
): asserts value is NonNullable<T> {
  if (value === undefined || value === null) {
    message ??= 'Value must not be undefined or null';
    throw new TypeError(message);
  }
}

We use the utility type NonNullable<T> to remove undefined and null from T – e.g.:

function getValue(map: Map<string, number>, key: string): number {
  const value = map.get(key);
  assertType<undefined | number>(value);
  assertNonNullable(value);
  assertType<number>(value);
  return value;
}
31.4.0.3 Example assertion function: adding a property to an object

The following assertion function adds a property to an object and updates its type accordingly:

function addName<T extends object>(obj: T, name: string)
: asserts obj is (T & HasName) { // (A)
  (obj as HasName).name = name;
}
type HasName = {
  name: string,
};

const point = { x: 0, y: 0 };
addName(point, 'zero');
assertType<{ x: number, y: number, name: string }>(point);

An intersection type S & T (such as the one in line A) has the properties of both type S and type T.

31.5 Alternatives to assertion functions

31.5.1 Technique: forced conversion

An assertion function narrows the type of an existing value. A forced conversion function returns an existing value with a new type – for example:

function forceNumber(value: unknown): number {
  if (typeof value !== 'number') {
    throw new TypeError();
  }
  return value;
}

const value1a: unknown = 123;
const value1b = forceNumber(value1a);
assertType<number>(value1b);

const value2: unknown = 'abc';
assert.throws(() => forceNumber(value2));

The corresponding assertion function looks as follows:

function assertIsNumber(value: unknown): asserts value is number {
  if (typeof value !== 'number') {
    throw new TypeError();
  }
}

const value1: unknown = 123;
assertIsNumber(value1);
assertType<number>(value1);

const value2: unknown = 'abc';
assert.throws(() => assertIsNumber(value2));

Forced conversion is a versatile technique with uses beyond those of assertion functions. For example, we can convert:

For more information, see “External vs. internal representation of data” (§32.3.4).

31.5.2 Technique: throwing an exception

Consider the following code:

function getLengthOfValue(strMap: Map<string, string>, key: string)
: number {
  if (strMap.has(key)) {
    const value = strMap.get(key);

    assertType<string | undefined>(value); // before type check
    // We know that value can’t be `undefined`
    if (value === undefined) { // (A)
      throw new Error();
    }
    assertType<string>(value); // after type check

    return value.length;
  }
  return -1;
}

Instead of the if statement that starts in line A, we also could have used an assertion function:

assertNonNullable(value);

Throwing an exception is a quick alternative if we don’t want to write such a function. Similarly to calling an assertion function, this technique also updates the static type.

31.6 Quick reference: assertion functions

31.6.1 Assertion signature: asserts «cond»

function assertTrue(condition: boolean): asserts condition {
  if (!condition) {
    throw new Error(); // assertion error
  }
}

31.6.2 Assertion signature: asserts «arg» is «type»

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error(); // assertion error
  }
}