TypeScript Assertion Functions for Runtime Validation
Use assertion functions to narrow types at runtime, replacing manual throw-and-cast patterns with compiler-understood safety checks.
What you'll learn
- ✓How assertion functions differ from type guards
- ✓Writing asserts condition and asserts value is Type signatures
- ✓Practical patterns for validating API responses and config
- ✓Combining assertion functions with discriminated unions
Prerequisites
None — this post is self-contained.
TypeScript has type guards that return a boolean to narrow types in conditional branches. Assertion functions take a different approach: they throw if a condition is not met, and if they return at all, the compiler knows the assertion holds for the rest of the scope. This matches how runtime validation actually works in most codebases, where invalid data should crash immediately rather than flow through an else branch.
The asserts Keyword
An assertion function uses the asserts keyword in its return type. There are two forms.
The first asserts that a condition is truthy:
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
After calling assert(x !== null, 'x is null'), TypeScript knows x is not null for the rest of the block.
The second form asserts that a value is a specific type:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`Expected string, got ${typeof value}`);
}
}
After calling assertIsString(input), the compiler treats input as string.
Assertion Functions vs Type Guards
A type guard returns boolean and narrows inside an if block:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function process(input: unknown) {
if (isString(input)) {
// input is string here
console.log(input.toUpperCase());
}
// input is still unknown here
}
An assertion function throws or narrows for the entire remaining scope:
function process(input: unknown) {
assertIsString(input);
// input is string from here onward
console.log(input.toUpperCase());
}
Use type guards when both branches are valid. Use assertion functions when the invalid case should halt execution.
Asserting Non-Null Values
The most common assertion function in practice validates that a value is neither null nor undefined:
function assertDefined<T>(
value: T | null | undefined,
name: string
): asserts value is T {
if (value === null || value === undefined) {
throw new Error(`Expected ${name} to be defined`);
}
}
This replaces the awkward pattern of checking and casting:
// Before: manual check with no narrowing benefit
const el = document.getElementById('app');
if (!el) throw new Error('Missing #app');
// el is still HTMLElement | null without assertion function
// After: assertion function narrows automatically
const el = document.getElementById('app');
assertDefined(el, '#app element');
// el is HTMLElement from here
el.classList.add('loaded');
Validating API Responses
Assertion functions are powerful for validating data from external sources like API responses:
interface User {
id: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
}
function assertIsUser(data: unknown): asserts data is User {
if (typeof data !== 'object' || data === null) {
throw new Error('Expected an object');
}
const obj = data as Record<string, unknown>;
if (typeof obj.id !== 'string') {
throw new Error('Expected id to be a string');
}
if (typeof obj.email !== 'string') {
throw new Error('Expected email to be a string');
}
if (!['admin', 'editor', 'viewer'].includes(obj.role as string)) {
throw new Error(`Invalid role: ${obj.role}`);
}
}
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
assertIsUser(data);
return data; // data is User
}
The assertion function acts as a validation boundary. Everything after the call benefits from full type safety.
Asserting Discriminated Unions
When working with discriminated unions, assertion functions can narrow to a specific variant:
type Result =
| { status: 'success'; data: string }
| { status: 'error'; message: string }
| { status: 'loading' };
function assertSuccess(
result: Result
): asserts result is { status: 'success'; data: string } {
if (result.status !== 'success') {
throw new Error(`Expected success, got ${result.status}`);
}
}
function processResult(result: Result) {
assertSuccess(result);
// result.data is available, result.message is not
console.log(result.data.toUpperCase());
}
Chaining Multiple Assertions
Assertion functions compose naturally because each one narrows the type for subsequent code:
interface Config {
port?: number;
host?: string;
database?: {
url?: string;
poolSize?: number;
};
}
function assertValidConfig(
config: Config
): asserts config is Required<Config> & {
database: Required<Config['database']>;
} {
assertDefined(config.port, 'config.port');
assertDefined(config.host, 'config.host');
assertDefined(config.database, 'config.database');
assertDefined(config.database.url, 'config.database.url');
assertDefined(config.database.poolSize, 'config.database.poolSize');
}
function startServer(config: Config) {
assertValidConfig(config);
// All properties are guaranteed to exist
console.log(`Starting on ${config.host}:${config.port}`);
console.log(`DB pool size: ${config.database.poolSize}`);
}
Generic Assertion Functions
You can write generic assertion functions that work with any type predicate:
function assertInstanceOf<T>(
value: unknown,
constructor: new (...args: any[]) => T,
message?: string
): asserts value is T {
if (!(value instanceof constructor)) {
throw new Error(
message ?? `Expected instance of ${constructor.name}`
);
}
}
function handleError(err: unknown) {
assertInstanceOf(err, Error);
// err is Error
console.error(err.message);
console.error(err.stack);
}
A generic array assertion:
function assertNonEmpty<T>(
arr: T[],
name: string
): asserts arr is [T, ...T[]] {
if (arr.length === 0) {
throw new Error(`Expected ${name} to be non-empty`);
}
}
function getFirst(items: string[]) {
assertNonEmpty(items, 'items');
// items is [string, ...string[]]
return items[0]; // string, not string | undefined
}
Rules and Limitations
Assertion functions must be declared with explicit function declarations or function expressions. Arrow functions do not support the asserts return type:
// This works
function assertTruthy(val: unknown): asserts val {
if (!val) throw new Error('Falsy');
}
// This does NOT work
const assertTruthy = (val: unknown): asserts val => {
if (!val) throw new Error('Falsy');
};
Assertion functions must either throw or return void. They cannot return a value. The compiler treats a normal return as proof that the assertion holds.
The function must be called directly. Passing an assertion function as a callback or storing it in a variable before calling it may prevent narrowing from working correctly.
When to Use Assertion Functions
Assertion functions are the right tool at boundaries where invalid data should halt execution: parsing API responses, loading configuration, processing CLI arguments, and validating environment variables at startup. They replace the pattern of checking a condition, throwing, and then relying on the compiler to understand that code after the throw is unreachable.
For situations where both outcomes are valid and you need to branch on the result, stick with type guards. Assertion functions are for the “this must be true or we cannot continue” case.
Wrap Up
Assertion functions give TypeScript a way to express runtime validation as type narrowing. The asserts return type tells the compiler that if the function returns normally, a condition holds for the rest of the scope. They eliminate the gap between runtime checks and compile-time types, making validation code both safe and concise.
Related articles
- TypeScript tsconfig.json Deep Dive: Every Option Explained
A comprehensive guide to tsconfig.json covering compiler options, module resolution, path aliases, project references, and recommended configurations.
- TypeScript TypeScript Generics: Advanced Patterns and Constraints
Master advanced TypeScript generics patterns including conditional constraints, recursive types, generic factories, and type-safe builder patterns with examples.
- TypeScript TypeScript Type Guards and Narrowing Explained
Learn how TypeScript type guards and control flow narrowing work, including typeof, instanceof, in, discriminated unions, and custom type predicates.
- TypeScript TypeScript Utility Types: The Complete Guide
A comprehensive guide to all built-in TypeScript utility types including Partial, Required, Pick, Omit, Record, Extract, Exclude, and more with practical examples.