Assert Conditions and Types

// #types#typescript // 1 comment

The asserts statement was introduced in TypeScript 3.7. It's a special type of function signature that tells the TypeScript compiler that a particular condition is true from that point on. Essentially, assertions serve as macros for if-then-error statements, allowing us to encapsulate precondition checks at the beginning of function blocks, enhancing the predictability and stability of our code.

Basic Assertions

Consider a basic assertion that checks for a truthy condition. Pay attention to the return type of the function.

function assert(condition: any, msg?: string): asserts condition { if (!condition) { throw new Error(msg); } }

The asserts condition return type within this function signals to TypeScript that, given the function's successful execution, the provided condition is true. Otherwise, an error will be thrown with the specified message.

Here's how this assert function can be used to check unknown parameters:

type Point = { x: number; y: number }; function point(x: unknown, y: unknown): Point { assert(typeof x === 'number', 'x is not a number'); assert(typeof y === 'number', 'y is not a number'); //> from here on, we know that `x` and `y` are numbers return { x, y }; }

TypeScript evaluates the condition typeof x === number and infers the appropriate type for the parameters. After the assert calls, TypeScript is aware that x and y are numbers.

Asserting Specific Types

Beyond asserting a condition, the asserts keyword can validate that a variable matches a specific type. This is achieved by appending a type guard after asserts.

Consider the following example:

function assertPoint(val: unknown): asserts val is Point { if (typeof val === 'object' && 'x' in val && 'y' in val && typeof val.x === 'number' && typeof val.y === 'number') { return; } throw new Error('val is not a Point'); }

If the assertPoint function executes without errors, TypeScript assumes that val is a Point. This knowledge is retained throughout the block, as demonstrated in this function:

function print(point: unknown) { assertPoint(point); //> from here on, we know that `p` is a Point console.log(`Position X=${point.x} Y={point.y}`); }

Asserting Complex Types

The asserts isn't confined to simple types or distinct conditions. It also enables us to assert more intricate types. One such example is ensuring a value is defined using TypeScript's NonNullable<T> utility type.

Let's consider the following example:

function assertNonNull<T>(val: T): asserts val is NonNullable<T> { if (val === undefined || val === null) { throw new Error(`val is ${val === undefined ? 'undefined' : 'null'}`); } }

Here, the assertNonNull function verifies that the supplied value is neither null nor undefined. The return type asserts val is NonNullable<T> signals to TypeScript that if the function successfully executes, val has a defined value.

Lastly, this example demonstrates how this assertion can be paired with the prior one to check multiple conditions:

function move(point?: unknown) { assertNonNull(point); assertPoint(point); // > from here on, we know that `point` is defined and is a Point console.log(`Moving to ${point.x}, ${point.y}`); }

Here, the two assertions at the beginning of the function help TypeScript to gain knowledge about the nature of the given parameter. After these conditions, TypeScript knows that point is defined and it's an object of type Point.

If you're intrigued by assertions and wish to learn more, I recommend exploring the GitHub PR that brought assertions into TypeScript. For a quick hands-on experience, head over to the Playground from Microsoft.