TypeScript's type system provides powerful compile-time safety, but runtime type checking remains essential when dealing with external data sources, user input, or third-party APIs. Assertion functions bridge this gap by allowing you to express type invariants that TypeScript can understand and validate.
Introduced in TypeScript 3.7, the asserts keyword enables developers to write functions that throw errors when conditions fail while simultaneously informing the TypeScript compiler about type narrowing. This guide explores how assertion functions work, when to use them, and best practices for integrating them into your codebase.
Whether you're building full-stack web applications or working with AI-powered solutions, mastering assertion functions significantly improves code reliability and maintainability.
Key concepts and practical skills covered in this guide
Understanding Assertion Functions
Learn how the asserts keyword works and how assertion functions differ from type guards
Runtime Type Validation
Validate API responses and external data with clear error messages
DOM Type Safety
Assert element types when working with querySelector and DOM APIs
Best Practices
Write maintainable assertions that serve as documentation
What Are Assertion Functions?
Assertion functions are a special type of function in TypeScript that use the asserts keyword to tell the TypeScript compiler that a certain condition must be true after the function executes. Unlike regular type guards that return a boolean and use the is keyword for type narrowing, assertion functions explicitly state their intent to throw an error if a condition is not met.
The key difference between type guards and assertion functions lies in their return type and how they communicate type information. A type guard returns value is Type and TypeScript narrows the type within the guard's conditional block. An assertion function uses asserts condition as its return type, meaning it asserts that a condition is true and throws an error if it is not.
Assertion functions serve as documentation for your code's assumptions. When you call an assertion function, you're making a promise to other developers--and to the TypeScript compiler--that certain conditions will be true at that point in the program. If those conditions are violated, the assertion function will throw an error, making bugs immediately apparent rather than allowing them to propagate through your system.
1function assertIsString(value: unknown): asserts value is string {2 if (typeof value !== 'string') {3 throw new Error(`Expected string, got ${typeof value}`);4 }5}The Assert Keyword Syntax
The asserts keyword is used in function return type positions to indicate that the function asserts a certain condition. The basic syntax involves declaring a function with a return type of asserts condition, where condition is some boolean expression that should evaluate to true.
In this example, the assertIsString function asserts that the value parameter is a string. If the assertion succeeds, TypeScript knows that value is a string within any code that follows the assertion call. This pattern can be extended to assert more complex conditions involving multiple variables or specific invariants that your code depends on.
The power of assertion functions lies in their ability to express complex type relationships that would be difficult or impossible to express with traditional type guards. For example, you can assert that one number is less than another, that an object has all required properties, or that a string matches a specific pattern--all while providing runtime validation through explicit error throwing.
Type Guards vs. Assertion Functions
Type guards and assertion functions serve complementary purposes in TypeScript's type system. Type guards are functions that return a boolean and use the is keyword to inform TypeScript about type narrowing. They are ideal for simple type checks that can be expressed as a single boolean condition.
Assertion functions, on the other hand, are better suited for expressing complex invariants that may involve multiple conditions, side effects, or the need to throw descriptive errors. The choice between type guards and assertion functions depends on your use case. Type guards are excellent for predicates that will be used in conditional expressions, as they naturally fit into if statements and other boolean contexts.
1// Type Guard2function isString(value: unknown): value is string {3 return typeof value === 'string';4}5 6// Assertion Function7function assertString(value: unknown): asserts value is string {8 if (typeof value !== 'string') {9 throw new Error(`Expected string, got ${typeof value}`);10 }11}12 13// Using type guard in conditional14if (isString(data)) {15 // TypeScript narrows data to string here16 console.log(data.toUpperCase());17}18 19// Using assertion function20assertString(data);21// TypeScript knows data is string here22console.log(data.toUpperCase());When to Use Assertion Functions
Assertion functions shine in several scenarios that are common in modern web development:
API Response Validation: When working with external APIs, you often receive data that TypeScript cannot fully type at compile time. Assertion functions allow you to validate this data and inform TypeScript about its actual shape, enabling full type safety throughout your application.
User Input Validation: When parsing user input or configuration files, assertion functions can validate the input and narrow types in a single step, providing clear error messages when validation fails.
Class Invariants: Assertion functions are excellent for enforcing class invariants--conditions that must be true for an object to be in a valid state. By calling assertion functions in constructor and setter methods, you ensure that objects never enter an invalid state.
Testing: Assertion functions can serve as custom matchers or validation helpers that provide clear, descriptive error messages when tests fail, making debugging easier.
Practical Examples
Validating API Responses
When working with external APIs, the data you receive is often typed as unknown or any because TypeScript cannot verify the shape of remote data at compile time. Assertion functions allow you to validate this data and inform TypeScript about its actual structure. This pattern is particularly valuable when building robust web applications that depend on external data sources.
1interface User {2 id: number;3 name: string;4 email: string;5}6 7function assertIsUser(data: unknown): asserts data is User {8 if (typeof data !== 'object' || data === null) {9 throw new Error('Data is not an object');10 }11 12 const obj = data as Record<string, unknown>;13 14 if (typeof obj.id !== 'number') {15 throw new Error('User must have a numeric id');16 }17 18 if (typeof obj.name !== 'string') {19 throw new Error('User must have a name');20 }21 22 if (typeof obj.email !== 'string') {23 throw new Error('User must have an email');24 }25}26 27// Usage with API response28async function fetchUser(userId: number): Promise<User> {29 const response = await fetch(`/api/users/${userId}`);30 const data = await response.json();31 32 // Validate and narrow the type33 assertIsUser(data);34 35 // TypeScript now knows data is User36 return data;37}DOM Manipulation
Working with the DOM often involves type assertions when accessing elements, as the querySelector method returns HTMLElement | null, which lacks the specific properties you need. Assertion functions can make these checks more explicit and reusable.
1function assertIsInputElement(2 element: HTMLElement | null3): asserts element is HTMLInputElement {4 if (!(element instanceof HTMLInputElement)) {5 throw new Error('Expected an HTMLInputElement');6 }7}8 9function assertIsDivElement(10 element: HTMLElement | null11): asserts element is HTMLDivElement {12 if (!(element instanceof HTMLDivElement)) {13 throw new Error('Expected an HTMLDivElement');14 }15}16 17// Usage in a form handler18function handleFormSubmit(event: Event): void {19 event.preventDefault();20 21 const nameInput = document.getElementById('name');22 const emailInput = document.getElementById('email');23 24 // Assert and narrow in one step25 assertIsInputElement(nameInput);26 assertIsInputElement(emailInput);27 28 // Both nameInput and emailInput are now HTMLInputElement29 const name = nameInput.value;30 const email = emailInput.value;31 32 console.log(`Submitting: ${name} (${email})`);33}Complex Validation
Assertion functions truly shine when validating complex conditions that involve multiple variables or business logic rules.
1interface Order {2 items: Array<{ productId: string; quantity: number }>;3 shippingAddress: string;4 paymentMethod: string;5}6 7function assertValidOrder(order: unknown): asserts order is Order {8 if (typeof order !== 'object' || order === null) {9 throw new Error('Order must be an object');10 }11 12 const o = order as Record<string, unknown>;13 14 if (!Array.isArray(o.items)) {15 throw new Error('Order must have an items array');16 }17 18 o.items.forEach((item, index) => {19 if (typeof item !== 'object' || item === null) {20 throw new Error(`Item at index ${index} is not an object`);21 }22 23 const i = item as Record<string, unknown>;24 25 if (typeof i.productId !== 'string') {26 throw new Error(`Item at index ${index} must have a string productId`);27 }28 29 if (typeof i.quantity !== 'number' || i.quantity <= 0) {30 throw new Error(`Item at index ${index} must have a positive quantity`);31 }32 });33 34 if (typeof o.shippingAddress !== 'string' || o.shippingAddress.trim().length === 0) {35 throw new Error('Order must have a valid shipping address');36 }37 38 const validPaymentMethods = ['credit_card', 'paypal', 'bank_transfer'];39 if (!validPaymentMethods.includes(o.paymentMethod as string)) {40 throw new Error(`Payment method must be one of: ${validPaymentMethods.join(', ')}`);41 }42}Best Practices for Assertion Functions
Provide Descriptive Error Messages
The error messages thrown by assertion functions are crucial for debugging and maintenance. When an assertion fails, the error message should clearly explain what was expected and what was received. This information helps developers quickly identify and fix the root cause of failures.
1// Good: Descriptive error message2function assertPositiveNumber(value: unknown): asserts value is number {3 if (typeof value !== 'number') {4 throw new Error(`Expected a number, but received ${typeof value}: ${value}`);5 }6 if (value <= 0) {7 throw new Error(`Expected a positive number, but received ${value}`);8 }9}10 11// Avoid: Vague error messages12function assertPositiveNumberBad(value: unknown): asserts value is number {13 if (typeof value !== 'number' || value <= 0) {14 throw new Error('Invalid value');15 }16}Keep Assertions Focused
Each assertion function should validate one specific condition or invariant. This makes them more reusable and easier to test. If you need to validate multiple conditions, compose smaller assertion functions together.
1// Focused assertions that can be composed2function assertString(value: unknown): asserts value is string {3 if (typeof value !== 'string') {4 throw new Error(`Expected string, got ${typeof value}`);5 }6}7 8function assertNonEmptyString(value: unknown): asserts value is string {9 assertString(value);10 if (value.trim().length === 0) {11 throw new Error('Expected non-empty string');12 }13}14 15function assertValidEmail(value: unknown): asserts value is string {16 assertNonEmptyString(value);17 if (!value.includes('@')) {18 throw new Error('Expected valid email address');19 }20}Use Assertion Functions for Invariants
Assertion functions are excellent for enforcing class invariants--conditions that must be true for an object to be in a valid state. By calling assertion functions in constructor and setter methods, you ensure that objects never enter an invalid state.
1class BankAccount {2 private _balance: number;3 4 constructor(initialBalance: number) {5 assertNonNegativeNumber(initialBalance);6 this._balance = initialBalance;7 }8 9 get balance(): number {10 return this._balance;11 }12 13 deposit(amount: number): void {14 assertPositiveNumber(amount);15 this._balance += amount;16 }17 18 withdraw(amount: number): void {19 assertPositiveNumber(amount);20 assertNonNegativeNumber(this._balance - amount);21 this._balance -= amount;22 }23}24 25function assertNonNegativeNumber(value: number): asserts value {26 if (value < 0) {27 throw new Error(`Expected non-negative number, got ${value}`);28 }29}Performance Considerations
Assertion functions add runtime overhead because they perform actual checks and may throw errors. However, this overhead is typically negligible compared to the benefits of catching bugs early. In performance-critical code paths, consider whether runtime validation is necessary or if compile-time type checking alone provides sufficient safety.
TypeScript's compiler removes type annotations during transpilation, but assertion functions contain actual runtime logic. This means assertion functions should be used judiciously in hot code paths. For validation that must happen in performance-critical sections, consider using type guards that return booleans instead, or implementing assertions only in development builds.
In most applications, the benefits of assertion functions far outweigh any performance concerns. The early detection of bugs, clearer error messages, and improved code documentation provide significant value that typically exceeds the minimal cost of runtime checks.
Common Pitfalls
Over-Using Assertion Functions
Not every type check needs an assertion function. For simple type checks in conditional expressions, type guards or simple typeof checks are often more appropriate. Reserve assertion functions for cases where you need to fail fast with a descriptive error or where you're expressing important invariants that should never be violated.
Asserting Too Broadly
Avoid assertions that are too broad or that make assumptions beyond what you can actually verify. If you assert that a value is a User object, make sure your assertion actually checks all the properties and types that a User is expected to have. Overly broad assertions can give false confidence and lead to runtime errors that are harder to diagnose.
Forgetting to Handle Assertion Failures
When calling assertion functions, consider how failures should be handled in your application. Assertion functions throw errors by design, so they should be called in contexts where throwing is acceptable. If you need optional validation that doesn't throw, consider creating a separate type guard version that returns a boolean.
Conclusion
Assertion functions are a powerful addition to TypeScript's type system that bridge the gap between compile-time type safety and runtime validation. By expressing invariants explicitly through assertion functions, you create code that is both safer and more self-documenting.
Whether you're validating API responses, enforcing class invariants, or checking complex business logic, assertion functions offer a flexible and expressive way to make your code's assumptions explicit. By following best practices--providing descriptive error messages, keeping assertions focused, and using them for genuine invariants--you can leverage assertion functions to write more robust and maintainable TypeScript applications.
Need help implementing robust type safety patterns in your web development projects? Our team of TypeScript experts can guide you through best practices and help you build more reliable applications.