Back to Blog
TypeScriptTypesAssertionsValidation

TypeScript Assertion Functions

Master TypeScript assertion functions. From type narrowing to runtime checks to invariants.

B
Bootspring Team
Engineering
May 29, 2020
7 min read

Assertion functions narrow types by throwing on invalid conditions. Here's how to use them.

Basic Assertion Functions#

1// Assertion function syntax 2function assert(condition: boolean, message?: string): asserts condition { 3 if (!condition) { 4 throw new Error(message || 'Assertion failed'); 5 } 6} 7 8// Usage - TypeScript narrows type after assertion 9function process(value: string | null) { 10 assert(value !== null, 'Value cannot be null'); 11 // TypeScript knows value is string here 12 console.log(value.toUpperCase()); 13} 14 15// Type predicate assertion 16function assertIsString(value: unknown): asserts value is string { 17 if (typeof value !== 'string') { 18 throw new TypeError(`Expected string, got ${typeof value}`); 19 } 20} 21 22// Usage 23function handleInput(input: unknown) { 24 assertIsString(input); 25 // TypeScript knows input is string 26 console.log(input.length); 27}

Common Assertion Functions#

1// Assert defined (not null/undefined) 2function assertDefined<T>( 3 value: T | null | undefined, 4 message?: string 5): asserts value is T { 6 if (value === null || value === undefined) { 7 throw new Error(message || 'Value is null or undefined'); 8 } 9} 10 11// Assert non-null 12function assertNonNull<T>( 13 value: T | null, 14 message?: string 15): asserts value is T { 16 if (value === null) { 17 throw new Error(message || 'Value is null'); 18 } 19} 20 21// Assert array 22function assertIsArray<T>( 23 value: unknown, 24 message?: string 25): asserts value is T[] { 26 if (!Array.isArray(value)) { 27 throw new TypeError(message || 'Value is not an array'); 28 } 29} 30 31// Assert number 32function assertIsNumber( 33 value: unknown, 34 message?: string 35): asserts value is number { 36 if (typeof value !== 'number' || isNaN(value)) { 37 throw new TypeError(message || 'Value is not a valid number'); 38 } 39} 40 41// Assert object 42function assertIsObject( 43 value: unknown, 44 message?: string 45): asserts value is Record<string, unknown> { 46 if (typeof value !== 'object' || value === null || Array.isArray(value)) { 47 throw new TypeError(message || 'Value is not an object'); 48 } 49}

Interface Assertions#

1interface User { 2 id: number; 3 name: string; 4 email: string; 5} 6 7function assertIsUser(value: unknown): asserts value is User { 8 if (typeof value !== 'object' || value === null) { 9 throw new TypeError('Expected object'); 10 } 11 12 const obj = value as Record<string, unknown>; 13 14 if (typeof obj.id !== 'number') { 15 throw new TypeError('Expected id to be a number'); 16 } 17 if (typeof obj.name !== 'string') { 18 throw new TypeError('Expected name to be a string'); 19 } 20 if (typeof obj.email !== 'string') { 21 throw new TypeError('Expected email to be a string'); 22 } 23} 24 25// Usage 26function processUser(data: unknown) { 27 assertIsUser(data); 28 // TypeScript knows data is User 29 console.log(`User: ${data.name} (${data.email})`); 30} 31 32// With validation details 33interface ValidationError { 34 field: string; 35 message: string; 36} 37 38function assertIsUserDetailed( 39 value: unknown 40): asserts value is User { 41 const errors: ValidationError[] = []; 42 43 if (typeof value !== 'object' || value === null) { 44 throw new Error('Expected object'); 45 } 46 47 const obj = value as Record<string, unknown>; 48 49 if (typeof obj.id !== 'number') { 50 errors.push({ field: 'id', message: 'Expected number' }); 51 } 52 if (typeof obj.name !== 'string') { 53 errors.push({ field: 'name', message: 'Expected string' }); 54 } 55 if (typeof obj.email !== 'string' || !obj.email.includes('@')) { 56 errors.push({ field: 'email', message: 'Expected valid email' }); 57 } 58 59 if (errors.length > 0) { 60 const error = new Error('Validation failed'); 61 (error as any).errors = errors; 62 throw error; 63 } 64}

Discriminated Union Assertions#

1type Shape = 2 | { kind: 'circle'; radius: number } 3 | { kind: 'rectangle'; width: number; height: number } 4 | { kind: 'triangle'; base: number; height: number }; 5 6function assertIsCircle( 7 shape: Shape 8): asserts shape is { kind: 'circle'; radius: number } { 9 if (shape.kind !== 'circle') { 10 throw new Error(`Expected circle, got ${shape.kind}`); 11 } 12} 13 14function assertIsRectangle( 15 shape: Shape 16): asserts shape is { kind: 'rectangle'; width: number; height: number } { 17 if (shape.kind !== 'rectangle') { 18 throw new Error(`Expected rectangle, got ${shape.kind}`); 19 } 20} 21 22// Generic assertion for discriminated unions 23function assertKind<T extends { kind: string }, K extends T['kind']>( 24 value: T, 25 kind: K 26): asserts value is Extract<T, { kind: K }> { 27 if (value.kind !== kind) { 28 throw new Error(`Expected ${kind}, got ${value.kind}`); 29 } 30} 31 32// Usage 33function processShape(shape: Shape) { 34 assertKind(shape, 'circle'); 35 // TypeScript knows shape is circle 36 console.log(`Area: ${Math.PI * shape.radius ** 2}`); 37}

Conditional Assertions#

1// Assert with condition 2function assertCondition<T>( 3 value: T | null | undefined, 4 condition: (value: T) => boolean, 5 message?: string 6): asserts value is T { 7 if (value === null || value === undefined) { 8 throw new Error(message || 'Value is null or undefined'); 9 } 10 if (!condition(value)) { 11 throw new Error(message || 'Condition not met'); 12 } 13} 14 15// Usage 16function processNumber(value: number | null) { 17 assertCondition(value, (n) => n > 0, 'Value must be positive'); 18 // TypeScript knows value is number (positive) 19 console.log(Math.sqrt(value)); 20} 21 22// Assert array not empty 23function assertNonEmptyArray<T>( 24 value: T[] 25): asserts value is [T, ...T[]] { 26 if (value.length === 0) { 27 throw new Error('Array is empty'); 28 } 29} 30 31// Usage 32function getFirst<T>(arr: T[]): T { 33 assertNonEmptyArray(arr); 34 return arr[0]; // No undefined in return type! 35}

Error Classes#

1// Custom assertion errors 2class AssertionError extends Error { 3 constructor(message: string) { 4 super(message); 5 this.name = 'AssertionError'; 6 } 7} 8 9class TypeAssertionError extends AssertionError { 10 constructor( 11 public expected: string, 12 public received: string 13 ) { 14 super(`Expected ${expected}, received ${received}`); 15 this.name = 'TypeAssertionError'; 16 } 17} 18 19function assertIsString(value: unknown): asserts value is string { 20 if (typeof value !== 'string') { 21 throw new TypeAssertionError('string', typeof value); 22 } 23} 24 25// With stack trace preservation 26function createAssertionError(message: string): Error { 27 const error = new AssertionError(message); 28 Error.captureStackTrace?.(error, createAssertionError); 29 return error; 30}

Runtime Invariants#

1// Invariant assertion (never returns normally if condition is false) 2function invariant( 3 condition: boolean, 4 message?: string 5): asserts condition { 6 if (!condition) { 7 throw new Error(`Invariant violation: ${message || 'Unexpected state'}`); 8 } 9} 10 11// Usage in state management 12class Counter { 13 private count: number = 0; 14 15 increment() { 16 this.count++; 17 invariant(this.count > 0, 'Count must be positive after increment'); 18 } 19 20 decrement() { 21 invariant(this.count > 0, 'Cannot decrement below zero'); 22 this.count--; 23 } 24} 25 26// Usage in unreachable code 27function exhaustiveCheck(value: never): never { 28 throw new Error(`Unhandled case: ${value}`); 29} 30 31type Status = 'pending' | 'active' | 'completed'; 32 33function handleStatus(status: Status) { 34 switch (status) { 35 case 'pending': 36 return 'Waiting...'; 37 case 'active': 38 return 'In progress'; 39 case 'completed': 40 return 'Done'; 41 default: 42 return exhaustiveCheck(status); 43 } 44}

Combining with Type Guards#

1// Type guard (returns boolean) 2function isString(value: unknown): value is string { 3 return typeof value === 'string'; 4} 5 6// Assertion (throws or narrows) 7function assertIsString(value: unknown): asserts value is string { 8 if (!isString(value)) { 9 throw new TypeError('Expected string'); 10 } 11} 12 13// Use type guard for conditional logic 14function maybeProcess(value: unknown) { 15 if (isString(value)) { 16 console.log(value.toUpperCase()); 17 } 18} 19 20// Use assertion for required values 21function mustProcess(value: unknown) { 22 assertIsString(value); 23 console.log(value.toUpperCase()); 24} 25 26// Convert guard to assertion 27function toAssertion<T>( 28 guard: (value: unknown) => value is T, 29 message?: string 30): (value: unknown) => asserts value is T { 31 return (value: unknown): asserts value is T => { 32 if (!guard(value)) { 33 throw new Error(message || 'Assertion failed'); 34 } 35 }; 36} 37 38const assertString = toAssertion(isString, 'Expected string');

API Response Validation#

1interface ApiResponse<T> { 2 success: boolean; 3 data?: T; 4 error?: string; 5} 6 7function assertSuccess<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 if (response.data === undefined) { 14 throw new Error('Response data is missing'); 15 } 16} 17 18// Usage 19async function fetchUser(id: string): Promise<User> { 20 const response = await api.get<User>(`/users/${id}`); 21 assertSuccess(response); 22 return response.data; 23} 24 25// Assert response shape 26function assertApiResponse<T>( 27 value: unknown, 28 dataValidator: (data: unknown) => asserts data is T 29): asserts value is ApiResponse<T> & { success: true; data: T } { 30 assertIsObject(value); 31 32 if (typeof value.success !== 'boolean') { 33 throw new Error('Missing success field'); 34 } 35 36 if (!value.success) { 37 throw new Error(String(value.error) || 'Request failed'); 38 } 39 40 dataValidator(value.data); 41}

Best Practices#

Design: ✓ Clear error messages ✓ Use custom error classes ✓ Preserve stack traces ✓ Document assertion behavior Usage: ✓ Validate at boundaries ✓ Assert invariants ✓ Use for required values ✓ Combine with type guards Performance: ✓ Assertions can be stripped in production ✓ Keep validation logic simple ✓ Avoid expensive checks in hot paths ✓ Consider lazy validation Avoid: ✗ Silent failures ✗ Overly complex assertions ✗ Assertions for flow control ✗ Ignoring assertion errors

Conclusion#

Assertion functions provide runtime type checking with TypeScript type narrowing. Use them for validating external data, enforcing invariants, and ensuring type safety at runtime. Combine with custom error classes and clear messages for better debugging.

Share this article

Help spread the word about Bootspring