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.
Assertions in control flow analysis #32695
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 explicitnever
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.
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!
Top comments (1)
This is really amazing. Love how you always produce such high quality content!