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:
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.
assert(cond)
for asserting conditionsFunction 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”.
assert.equal(value1, value2)
for asserting equalityThe 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.
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.
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:
condition
is boolean
and not unknown
because that forces us to avoid truthiness checks.
Error
, e.g. one called AssertionError
.
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>
.
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
.
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.
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;
}
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
.
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).
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.
asserts «cond»
function assertTrue(condition: boolean): asserts condition {
if (!condition) {
throw new Error(); // assertion error
}
}
asserts condition
void
or exception
asserts «arg» is «type»
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(); // assertion error
}
}
asserts value is string
void
or exception