Back to Blog
TypeScriptAssertionsTypesValidation

TypeScript Assertion Functions Guide

Master TypeScript assertion functions for runtime validation and type narrowing.

B
Bootspring Team
Engineering
July 27, 2018
8 min read

Assertion functions validate conditions at runtime and narrow types, throwing errors when assertions fail.

Basic Assertion Functions#

1// Assertion function signature 2function assert(condition: boolean, message?: string): asserts condition { 3 if (!condition) { 4 throw new Error(message || 'Assertion failed'); 5 } 6} 7 8// Usage 9function divide(a: number, b: number): number { 10 assert(b !== 0, 'Cannot divide by zero'); 11 return a / b; 12} 13 14// After assertion, condition is known to be true 15function processArray(arr: number[] | null) { 16 assert(arr !== null, 'Array must not be null'); 17 // TypeScript knows arr is number[] here 18 return arr.map(x => x * 2); 19}

Type Assertion Functions#

1// Assert value is specific type 2function assertIsString(value: unknown): asserts value is string { 3 if (typeof value !== 'string') { 4 throw new TypeError(`Expected string, got ${typeof value}`); 5 } 6} 7 8function processValue(value: unknown) { 9 assertIsString(value); 10 // TypeScript knows value is string here 11 console.log(value.toUpperCase()); 12} 13 14// Assert value is not null/undefined 15function assertDefined<T>( 16 value: T | null | undefined, 17 message?: string 18): asserts value is T { 19 if (value === null || value === undefined) { 20 throw new Error(message || 'Value must be defined'); 21 } 22} 23 24function getUser(id: string) { 25 const user = users.get(id); 26 assertDefined(user, `User ${id} not found`); 27 // TypeScript knows user is User, not User | undefined 28 return user.name; 29}

Object Type Assertions#

1// Assert object has specific shape 2interface User { 3 id: number; 4 name: string; 5 email: string; 6} 7 8function assertIsUser(value: unknown): asserts value is User { 9 if (typeof value !== 'object' || value === null) { 10 throw new TypeError('Expected object'); 11 } 12 13 const obj = value as Record<string, unknown>; 14 15 if (typeof obj.id !== 'number') { 16 throw new TypeError('id must be a number'); 17 } 18 if (typeof obj.name !== 'string') { 19 throw new TypeError('name must be a string'); 20 } 21 if (typeof obj.email !== 'string') { 22 throw new TypeError('email must be a string'); 23 } 24} 25 26function processApiResponse(data: unknown) { 27 assertIsUser(data); 28 // TypeScript knows data is User 29 console.log(`Welcome, ${data.name}!`); 30} 31 32// Generic object assertion 33function assertHasProperty<K extends string>( 34 obj: object, 35 key: K 36): asserts obj is object & Record<K, unknown> { 37 if (!(key in obj)) { 38 throw new Error(`Object missing property: ${key}`); 39 } 40}

Array Assertions#

1// Assert non-empty array 2function assertNonEmpty<T>( 3 arr: T[], 4 message?: string 5): asserts arr is [T, ...T[]] { 6 if (arr.length === 0) { 7 throw new Error(message || 'Array must not be empty'); 8 } 9} 10 11function processItems(items: number[]) { 12 assertNonEmpty(items, 'Need at least one item'); 13 // TypeScript knows items has at least one element 14 const [first, ...rest] = items; 15 console.log('First:', first); // first is number, not number | undefined 16} 17 18// Assert array of specific type 19function assertIsStringArray( 20 value: unknown 21): asserts value is string[] { 22 if (!Array.isArray(value)) { 23 throw new TypeError('Expected array'); 24 } 25 if (!value.every(item => typeof item === 'string')) { 26 throw new TypeError('All items must be strings'); 27 } 28}

Combining with Type Guards#

1// Type guard (returns boolean) 2function isUser(value: unknown): value is User { 3 return ( 4 typeof value === 'object' && 5 value !== null && 6 'id' in value && 7 'name' in value 8 ); 9} 10 11// Assertion function (throws or narrows) 12function assertIsUser(value: unknown): asserts value is User { 13 if (!isUser(value)) { 14 throw new TypeError('Invalid user object'); 15 } 16} 17 18// Use guard for conditional logic 19function maybeProcess(data: unknown) { 20 if (isUser(data)) { 21 // Optional path 22 console.log(data.name); 23 } 24} 25 26// Use assertion for required validation 27function mustProcess(data: unknown) { 28 assertIsUser(data); 29 // Required - throws if not user 30 console.log(data.name); 31}

Class Instance Assertions#

1// Assert instance of class 2function assertInstanceOf<T>( 3 value: unknown, 4 constructor: new (...args: any[]) => T, 5 message?: string 6): asserts value is T { 7 if (!(value instanceof constructor)) { 8 throw new TypeError( 9 message || `Expected instance of ${constructor.name}` 10 ); 11 } 12} 13 14class ApiError extends Error { 15 constructor(public code: number, message: string) { 16 super(message); 17 } 18} 19 20function handleError(error: unknown) { 21 assertInstanceOf(error, ApiError, 'Expected API error'); 22 // TypeScript knows error is ApiError 23 console.log(`API Error ${error.code}: ${error.message}`); 24}

Validation Chains#

1// Build validation chains 2class Validator<T> { 3 constructor(private value: T) {} 4 5 assert( 6 condition: boolean, 7 message: string 8 ): Validator<T> { 9 if (!condition) { 10 throw new Error(message); 11 } 12 return this; 13 } 14 15 assertString(): Validator<T & string> { 16 if (typeof this.value !== 'string') { 17 throw new TypeError('Expected string'); 18 } 19 return this as Validator<T & string>; 20 } 21 22 assertMinLength(min: number): this { 23 if (String(this.value).length < min) { 24 throw new Error(`Minimum length is ${min}`); 25 } 26 return this; 27 } 28 29 get(): T { 30 return this.value; 31 } 32} 33 34function validate<T>(value: T): Validator<T> { 35 return new Validator(value); 36} 37 38// Usage 39function processUsername(input: unknown) { 40 const username = validate(input) 41 .assertString() 42 .assertMinLength(3) 43 .get(); 44 45 // username is string 46 return username.toLowerCase(); 47}

Form Validation#

1interface FormData { 2 email: string; 3 password: string; 4 age: number; 5} 6 7function assertValidEmail(email: unknown): asserts email is string { 8 assertIsString(email); 9 if (!email.includes('@')) { 10 throw new Error('Invalid email format'); 11 } 12} 13 14function assertValidPassword(password: unknown): asserts password is string { 15 assertIsString(password); 16 if (password.length < 8) { 17 throw new Error('Password must be at least 8 characters'); 18 } 19} 20 21function assertValidAge(age: unknown): asserts age is number { 22 if (typeof age !== 'number' || isNaN(age)) { 23 throw new Error('Age must be a number'); 24 } 25 if (age < 0 || age > 150) { 26 throw new Error('Age must be between 0 and 150'); 27 } 28} 29 30function validateForm(data: unknown): FormData { 31 if (typeof data !== 'object' || data === null) { 32 throw new Error('Invalid form data'); 33 } 34 35 const { email, password, age } = data as Record<string, unknown>; 36 37 assertValidEmail(email); 38 assertValidPassword(password); 39 assertValidAge(age); 40 41 return { email, password, age }; 42}

API Response Validation#

1interface ApiResponse<T> { 2 success: boolean; 3 data: T; 4 error?: string; 5} 6 7function assertSuccessResponse<T>( 8 response: ApiResponse<T> 9): asserts response is ApiResponse<T> & { success: true; data: T } { 10 if (!response.success) { 11 throw new Error(response.error || 'Request failed'); 12 } 13} 14 15async function fetchUser(id: string): Promise<User> { 16 const response: ApiResponse<User> = await fetch(`/api/users/${id}`) 17 .then(r => r.json()); 18 19 assertSuccessResponse(response); 20 21 // TypeScript knows response.data is User 22 return response.data; 23} 24 25// Discriminated union version 26type Result<T> = 27 | { ok: true; value: T } 28 | { ok: false; error: string }; 29 30function assertOk<T>(result: Result<T>): asserts result is { ok: true; value: T } { 31 if (!result.ok) { 32 throw new Error(result.error); 33 } 34}

Environment Assertions#

1// Assert environment variables 2function assertEnv( 3 key: string 4): asserts process.env is Record<string, string> & Record<typeof key, string> { 5 if (!process.env[key]) { 6 throw new Error(`Missing environment variable: ${key}`); 7 } 8} 9 10function getConfig() { 11 assertEnv('DATABASE_URL'); 12 assertEnv('API_KEY'); 13 assertEnv('SECRET'); 14 15 return { 16 databaseUrl: process.env.DATABASE_URL, 17 apiKey: process.env.API_KEY, 18 secret: process.env.SECRET 19 }; 20} 21 22// Generic environment helper 23function requireEnv(key: string): string { 24 const value = process.env[key]; 25 if (!value) { 26 throw new Error(`Missing environment variable: ${key}`); 27 } 28 return value; 29}

Error Handling#

1// Narrow error types 2function assertError(value: unknown): asserts value is Error { 3 if (!(value instanceof Error)) { 4 throw new TypeError('Expected Error instance'); 5 } 6} 7 8async function safeOperation() { 9 try { 10 await riskyOperation(); 11 } catch (error) { 12 assertError(error); 13 // TypeScript knows error is Error 14 console.error('Operation failed:', error.message); 15 console.error('Stack:', error.stack); 16 } 17} 18 19// Custom error assertion 20interface ApiError { 21 code: string; 22 message: string; 23 details?: Record<string, string>; 24} 25 26function assertApiError(value: unknown): asserts value is ApiError { 27 if (typeof value !== 'object' || value === null) { 28 throw new TypeError('Expected object'); 29 } 30 31 const obj = value as Record<string, unknown>; 32 if (typeof obj.code !== 'string' || typeof obj.message !== 'string') { 33 throw new TypeError('Invalid API error shape'); 34 } 35}

Best Practices#

1// Clear error messages 2function assertPositive(n: number): asserts n is number { 3 if (n <= 0) { 4 throw new RangeError( 5 `Expected positive number, got ${n}` 6 ); 7 } 8} 9 10// Document assertion behavior 11/** 12 * Asserts that value is a valid user ID. 13 * @throws {TypeError} If value is not a string 14 * @throws {Error} If value is empty or invalid format 15 */ 16function assertValidUserId(value: unknown): asserts value is string { 17 // Implementation 18} 19 20// Use specific error types 21class ValidationError extends Error { 22 constructor(public field: string, message: string) { 23 super(message); 24 this.name = 'ValidationError'; 25 } 26} 27 28function assertValidName(name: unknown): asserts name is string { 29 if (typeof name !== 'string') { 30 throw new ValidationError('name', 'Must be a string'); 31 } 32 if (name.length < 2) { 33 throw new ValidationError('name', 'Must be at least 2 characters'); 34 } 35}

Conclusion#

Assertion functions validate conditions at runtime and narrow TypeScript types in a single operation. Use them when validation failure should throw an error, unlike type guards which return boolean for conditional paths. Create specific assertion functions for your domain types, use descriptive error messages, and consider custom error types for better error handling. Combine with type guards when you need both validation patterns.

Share this article

Help spread the word about Bootspring