Assertions: How to Assert Conditions and Types

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 thePlayground from Microsoft.

With this PR we reflect the effects of calls to assert(…) functions and never-returning functions in control flow analysis. We also improve analysis of the effects of exhaustive switch statements, and report unreachable code errors for statements that follow calls to never-returning functions or exhaustive switch statements that return or throw in all cases.

The PR introduces a new asserts modifier that can be used in type predicates:

declare function assert(value: unknown): asserts value;
declare function assertIsArrayOfStrings(obj: unknown): asserts obj is string[];
declare function assertNonNull<T>(obj: T): asserts obj is NonNullable<T>;

 

An asserts return type predicate indicates that the function returns only when the assertion holds and otherwise throws an exception. Specifically, the assert x form indicates that the function returns only when x is truthy, and the assert x is T form indicates that the function returns only when x is of type T. An asserts return type predicate implies that the returned value is of type void, and there is no provision for returning values of other types.

The effects of calls to functions with asserts type predicates are reflected in control flow analysis. For example:

function f1(x: unknown) {
assert(typeof x === “string”);
return x.length; // x has type string here
}

function f2(x: unknown) {
assertIsArrayOfStrings(x);
return x[0].length; // x has type string[] here
}

function f3(x: string | undefined) {
assertNonNull(x);
return x.length; // x has type string here
}

 

From a control flow analysis perspective, a call to a function with an asserts x return type is equivalent to an if statement that throws when x is falsy. For example, the control flow of f1 above is analyzed equivalently to

function f1(x: unknown) {
if (!(typeof x === “string”)) {
throw …;
}
return x.length; // x has type string here
}

 

Similarly, a call to a function with an asserts x is T return type is equivalent to an if statement that throws when a call to a function with an x is T return type returns false. In other words, given

declare function isArrayOfStrings(obj: unknown): obj is string[];

 

the control flow of f2 above is analyzed equivalently to

function f2(x: unknown) {
if (!isArrayOfStrings(x)) {
throw …;
}
return x[0].length; // x has type string[] here
}

 

Effectively, assertIsArrayOfStrings(x) is just shorthand for assert(isArrayOfStrings(x)).

In addition to support for asserts, we now reflect effects of calls to never-returning functions in control flow analysis.

function fail(message?: string): never {
throw new Error(message);
}

function f3(x: string | undefined) {
if (x === undefined) fail(“undefined argument”);
x.length; // Type narrowed to string
}

function f4(x: number): number {
if (x >= 0) return x;
fail(“negative number”);
}

function f5(x: number): number {
if (x >= 0) return x;
fail(“negative number”);
x; // Unreachable code error
}

 

Note that f4 is considered to not have an implicit return that contributes undefined to the return value. Without the call to fail an error would have been reported.

A function call is analyzed as an assertion call or never-returning call when

the call occurs as a top-level expression statement, and
the call specifies a single identifier or a dotted sequence of identifiers for the function name, and
each identifier in the function name references an entity with an explicit type, and
the function name resolves to a function type with an asserts return type or an explicit never return type annotation.

An entity is considered to have an explicit type when it is declared as a function, method, class or namespace, or as a variable, parameter or property with an explicit type annotation. (This particular rule exists so that control flow analysis of potential assertion calls doesn’t circularly trigger further analysis.)

EDIT: Updated to include effects of calls to never-returning functions.

Fixes #8655. Fixes #11572. Fixes #12668. Fixes #13241. Fixes #18362. Fixes #20409. Fixes #20823. Fixes #22470. Fixes #27909. Fixes #27388. Fixes #30000.

View on GitHub

I hope you found this post helpful. If you have any questions or comments, feel free to leave them below. If you’d like to connect with me, you can find me on LinkedIn or GitHub. Thanks for reading!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these <abbr title="HyperText Markup Language">HTML</abbr> tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>