Assert Conditions and Types
// 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.