+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 92 of 355

Assertion Functions: Custom Type Assertions

Master assertion functions in TypeScript to create powerful type assertion systems that throw errors for invalid types while providing compile-time type narrowing 🚀

💎Advanced
22 min read

Prerequisites

  • Solid understanding of type predicates and type guards 📝
  • Experience with error handling and function signatures ⚡
  • Knowledge of TypeScript's control flow analysis 💻

What you'll learn

  • Create custom assertion functions with the asserts keyword 🎯
  • Build type-safe validation systems that throw informative errors 🏗️
  • Implement runtime checks with automatic type narrowing 🐛
  • Apply assertion functions to real-world validation scenarios ✨

🎯 Introduction

Welcome to the assertive world of TypeScript! ⚡ This tutorial explores the powerful realm of assertion functions, a specialized form of type guards that not only check types at runtime but also throw errors when validation fails, providing both type safety and error handling in one elegant solution.

You’ll discover how to create custom assertion functions that use the asserts keyword to provide automatic type narrowing while ensuring your code fails fast when encountering invalid data. Whether you’re building robust APIs 🌐, validating user input 📝, or ensuring data integrity throughout your application 🛡️, assertion functions provide a powerful tool for defensive programming.

By the end of this tutorial, you’ll be crafting assertion systems that make your code both more reliable and more expressive! Let’s assert our way to type safety! 💪

📚 Understanding Assertion Functions

🤔 What Are Assertion Functions?

Assertion functions are special functions that use the asserts keyword in their return type to tell TypeScript that if the function returns normally (doesn’t throw), then a certain condition is true. They combine type checking with error throwing:

// 🌟 Basic assertion function syntax
function assert(condition: any, message?: string): asserts condition {
  if (!condition) {
    throw new Error(message || 'Assertion failed');
  }
}

// 🎯 Using the assertion function
function processValue(input: unknown) {
  assert(typeof input === 'string', 'Expected string input');
  
  // ✨ TypeScript now knows input is string
  console.log(input.toUpperCase()); // No type error!
  console.log(input.length);        // Access string properties
}

// 🔍 Type-specific assertion
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function handleString(input: unknown) {
  assertIsString(input);
  
  // ✨ TypeScript knows input is string after assertion
  return input.substring(0, 10);
}

// 🌟 Comparison with type predicates
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function compareApproaches(input: unknown) {
  // Type predicate approach
  if (isString(input)) {
    console.log(input.toUpperCase());
  } else {
    // Need manual error handling
    throw new Error('Expected string');
  }
  
  // Assertion function approach
  assertIsString(input);
  console.log(input.toUpperCase()); // Cleaner, automatic narrowing
}

🏗️ Assertion Function Fundamentals

// 🎯 Basic assertion functions for primitives
function assertIsNumber(value: unknown): asserts value is number {
  if (typeof value !== 'number' || isNaN(value)) {
    throw new TypeError(`Expected number, got ${typeof value}`);
  }
}

function assertIsBoolean(value: unknown): asserts value is boolean {
  if (typeof value !== 'boolean') {
    throw new TypeError(`Expected boolean, got ${typeof value}`);
  }
}

function assertIsFunction(value: unknown): asserts value is Function {
  if (typeof value !== 'function') {
    throw new TypeError(`Expected function, got ${typeof value}`);
  }
}

// 🌟 Array assertion functions
function assertIsArray(value: unknown): asserts value is unknown[] {
  if (!Array.isArray(value)) {
    throw new TypeError(`Expected array, got ${typeof value}`);
  }
}

function assertIsStringArray(value: unknown): asserts value is string[] {
  assertIsArray(value);
  value.forEach((item, index) => {
    if (typeof item !== 'string') {
      throw new TypeError(`Expected string at index ${index}, got ${typeof item}`);
    }
  });
}

function assertIsNumberArray(value: unknown): asserts value is number[] {
  assertIsArray(value);
  value.forEach((item, index) => {
    if (typeof item !== 'number' || isNaN(item)) {
      throw new TypeError(`Expected number at index ${index}, got ${typeof item}`);
    }
  });
}

// ✅ Using assertion functions
function demonstrateAssertions(data: unknown) {
  try {
    assertIsNumber(data);
    console.log(data.toFixed(2));        // ✨ Number methods available
  } catch (error) {
    console.log('Not a number:', error.message);
  }
  
  try {
    assertIsStringArray(data);
    console.log(data.map(s => s.toUpperCase())); // ✨ String array methods
  } catch (error) {
    console.log('Not a string array:', error.message);
  }
}

// 🔄 Null and undefined assertions
function assertIsNotNull<T>(value: T | null): asserts value is T {
  if (value === null) {
    throw new Error('Value is null');
  }
}

function assertIsNotUndefined<T>(value: T | undefined): asserts value is T {
  if (value === undefined) {
    throw new Error('Value is undefined');
  }
}

function assertIsNotNullish<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(`Value is ${value}`);
  }
}

// 🎨 Using nullish assertions
function processData(input: string | null | undefined) {
  assertIsNotNullish(input);
  
  // ✨ TypeScript knows input is string
  console.log(input.substring(0, 10));
}

// 🌟 Custom error types for assertions
class AssertionError extends Error {
  constructor(message: string, public readonly expected: string, public readonly actual: string) {
    super(message);
    this.name = 'AssertionError';
  }
}

function assertWithCustomError<T>(
  condition: any,
  message: string,
  expected: string,
  actual: string
): asserts condition {
  if (!condition) {
    throw new AssertionError(message, expected, actual);
  }
}

function assertIsPositiveNumber(value: unknown): asserts value is number {
  assertWithCustomError(
    typeof value === 'number' && !isNaN(value) && value> 0,
    'Expected positive number',
    'positive number',
    `${typeof value} (${value})`
  );
}

🧮 Complex Object Assertions

// 🎯 Object structure assertions
interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

function assertIsUser(value: unknown): asserts value is User {
  if (typeof value !== 'object' || value === null) {
    throw new TypeError('Expected object');
  }
  
  const obj = value as any;
  
  if (typeof obj.id !== 'number') {
    throw new TypeError(`Expected number for id, got ${typeof obj.id}`);
  }
  
  if (typeof obj.name !== 'string') {
    throw new TypeError(`Expected string for name, got ${typeof obj.name}`);
  }
  
  if (typeof obj.email !== 'string') {
    throw new TypeError(`Expected string for email, got ${typeof obj.email}`);
  }
}

function assertIsProduct(value: unknown): asserts value is Product {
  if (typeof value !== 'object' || value === null) {
    throw new TypeError('Expected object');
  }
  
  const obj = value as any;
  
  if (typeof obj.id !== 'number') {
    throw new TypeError(`Expected number for id, got ${typeof obj.id}`);
  }
  
  if (typeof obj.title !== 'string') {
    throw new TypeError(`Expected string for title, got ${typeof obj.title}`);
  }
  
  if (typeof obj.price !== 'number') {
    throw new TypeError(`Expected number for price, got ${typeof obj.price}`);
  }
}

// 🌟 Generic property assertions
function assertHasProperty<K extends string>(
  obj: unknown,
  key: K
): asserts obj is Record<K, unknown> {
  if (typeof obj !== 'object' || obj === null) {
    throw new TypeError('Expected object');
  }
  
  if (!(key in obj)) {
    throw new Error(`Missing required property: ${key}`);
  }
}

function assertHasStringProperty<K extends string>(
  obj: unknown,
  key: K
): asserts obj is Record<K, string> {
  assertHasProperty(obj, key);
  
  const value = (obj as any)[key];
  if (typeof value !== 'string') {
    throw new TypeError(`Expected string for ${key}, got ${typeof value}`);
  }
}

function assertHasNumberProperty<K extends string>(
  obj: unknown,
  key: K
): asserts obj is Record<K, number> {
  assertHasProperty(obj, key);
  
  const value = (obj as any)[key];
  if (typeof value !== 'number') {
    throw new TypeError(`Expected number for ${key}, got ${typeof value}`);
  }
}

// ✅ Using object assertions
function processEntity(data: unknown) {
  try {
    assertIsUser(data);
    // ✨ TypeScript knows data is User
    console.log(`User: ${data.name} (${data.email})`);
  } catch (error) {
    try {
      assertIsProduct(data);
      // ✨ TypeScript knows data is Product
      console.log(`Product: ${data.title} - $${data.price}`);
    } catch (error2) {
      console.log('Invalid entity format');
    }
  }
}

function validateObject(obj: unknown) {
  assertHasStringProperty(obj, 'name');
  // ✨ TypeScript knows obj has name: string
  console.log(`Name: ${obj.name}`);
  
  assertHasNumberProperty(obj, 'age');
  // ✨ TypeScript knows obj has age: number
  console.log(`Age: ${obj.age} years old`);
}

🔄 Advanced Assertion Patterns

🎯 Discriminated Union Assertions

// 🌟 Discriminated union types
interface LoadingState {
  status: 'loading';
  progress: number;
}

interface SuccessState {
  status: 'success';
  data: string;
}

interface ErrorState {
  status: 'error';
  message: string;
  code: number;
}

type AppState = LoadingState | SuccessState | ErrorState;

// 🔍 Discriminated union assertions
function assertIsLoadingState(state: AppState): asserts state is LoadingState {
  if (state.status !== 'loading') {
    throw new Error(`Expected loading state, got ${state.status}`);
  }
}

function assertIsSuccessState(state: AppState): asserts state is SuccessState {
  if (state.status !== 'success') {
    throw new Error(`Expected success state, got ${state.status}`);
  }
}

function assertIsErrorState(state: AppState): asserts state is ErrorState {
  if (state.status !== 'error') {
    throw new Error(`Expected error state, got ${state.status}`);
  }
}

// ✨ Using discriminated union assertions
function handleAppState(state: AppState) {
  try {
    assertIsLoadingState(state);
    console.log(`Loading: ${state.progress}%`);
  } catch {
    try {
      assertIsSuccessState(state);
      console.log(`Success: ${state.data}`);
    } catch {
      assertIsErrorState(state);
      console.log(`Error ${state.code}: ${state.message}`);
    }
  }
}

// 🎨 Generic status assertion
function assertHasStatus<T extends string>(
  state: { status: string },
  expectedStatus: T
): asserts state is { status: T } & typeof state {
  if (state.status !== expectedStatus) {
    throw new Error(`Expected status ${expectedStatus}, got ${state.status}`);
  }
}

function handleStateGeneric(state: AppState) {
  assertHasStatus(state, 'loading');
  // ✨ TypeScript knows it's LoadingState
  console.log(`Progress: ${state.progress}`);
}

// 🔄 Complex discriminated unions with validation
type APIResult<T> = 
  | { type: 'success'; data: T; timestamp: string }
  | { type: 'error'; error: string; code: number }
  | { type: 'pending'; startTime: string };

function assertIsSuccessResult<T>(
  result: APIResult<T>,
  dataValidator: (data: unknown) => asserts data is T
): asserts result is Extract<APIResult<T>, { type: 'success' }> {
  if (result.type !== 'success') {
    throw new Error(`Expected success result, got ${result.type}`);
  }
  
  dataValidator(result.data);
}

function assertIsErrorResult<T>(
  result: APIResult<T>
): asserts result is Extract<APIResult<T>, { type: 'error' }> {
  if (result.type !== 'error') {
    throw new Error(`Expected error result, got ${result.type}`);
  }
}

// Usage with data validation
function processAPIResult(result: APIResult<User>) {
  try {
    assertIsSuccessResult(result, assertIsUser);
    // ✨ TypeScript knows result.data is User
    console.log(`Welcome ${result.data.name}!`);
  } catch {
    try {
      assertIsErrorResult(result);
      // ✨ TypeScript knows result has error info
      console.log(`Error ${result.code}: ${result.error}`);
    } catch {
      // Must be pending
      console.log('Request is pending...');
    }
  }
}

🧩 Class Instance Assertions

// 🎯 Class-based assertions
class NetworkError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public url: string
  ) {
    super(message);
    this.name = 'NetworkError';
  }
}

class ValidationError extends Error {
  constructor(
    message: string,
    public field: string,
    public value: unknown
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

// 🔍 Instance assertion functions
function assertIsNetworkError(error: unknown): asserts error is NetworkError {
  if (!(error instanceof NetworkError)) {
    throw new TypeError(`Expected NetworkError, got ${error?.constructor?.name || typeof error}`);
  }
}

function assertIsValidationError(error: unknown): asserts error is ValidationError {
  if (!(error instanceof ValidationError)) {
    throw new TypeError(`Expected ValidationError, got ${error?.constructor?.name || typeof error}`);
  }
}

function assertIsError(value: unknown): asserts value is Error {
  if (!(value instanceof Error)) {
    throw new TypeError(`Expected Error, got ${typeof value}`);
  }
}

// ✨ Using class assertions
function handleError(error: unknown) {
  try {
    assertIsNetworkError(error);
    console.log(`Network error ${error.statusCode} at ${error.url}: ${error.message}`);
  } catch {
    try {
      assertIsValidationError(error);
      console.log(`Validation error in ${error.field}: ${error.message}`);
    } catch {
      try {
        assertIsError(error);
        console.log(`Generic error: ${error.message}`);
      } catch {
        console.log('Unknown error type:', error);
      }
    }
  }
}

// 🌟 Generic instance assertion
function assertIsInstanceOf<T extends new (...args: any[]) => any>(
  value: unknown,
  constructor: T
): asserts value is InstanceType<T> {
  if (!(value instanceof constructor)) {
    throw new TypeError(
      `Expected instance of ${constructor.name}, got ${value?.constructor?.name || typeof value}`
    );
  }
}

// Usage with generic instance assertion
function processValue(value: unknown) {
  assertIsInstanceOf(value, Date);
  console.log(value.toISOString()); // ✨ Date methods available
  
  assertIsInstanceOf(value, RegExp);
  console.log(value.source);        // ✨ RegExp properties available
}

// 🎨 Built-in type assertions
function assertIsDate(value: unknown): asserts value is Date {
  if (!(value instanceof Date) || isNaN(value.getTime())) {
    throw new TypeError(`Expected valid Date, got ${typeof value}`);
  }
}

function assertIsRegExp(value: unknown): asserts value is RegExp {
  if (!(value instanceof RegExp)) {
    throw new TypeError(`Expected RegExp, got ${typeof value}`);
  }
}

function assertIsPromise<T = unknown>(value: unknown): asserts value is Promise<T> {
  if (!(value instanceof Promise) && 
      !(typeof value === 'object' && 
        value !== null && 
        typeof (value as any).then === 'function')) {
    throw new TypeError(`Expected Promise, got ${typeof value}`);
  }
}

// 🔄 Assertion with custom validation
function assertIsValidDate(value: unknown, options?: {
  minDate?: Date;
  maxDate?: Date;
}): asserts value is Date {
  assertIsDate(value);
  
  if (options?.minDate && value < options.minDate) {
    throw new RangeError(`Date ${value.toISOString()} is before minimum ${options.minDate.toISOString()}`);
  }
  
  if (options?.maxDate && value> options.maxDate) {
    throw new RangeError(`Date ${value.toISOString()} is after maximum ${options.maxDate.toISOString()}`);
  }
}

function assertIsValidEmail(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new TypeError(`Expected string, got ${typeof value}`);
  }
  
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(value)) {
    throw new Error(`Invalid email format: ${value}`);
  }
}

🔄 Nested and Composite Assertions

// 🎯 Nested object assertions
interface Address {
  street: string;
  city: string;
  zipCode: string;
  country: string;
}

interface Person {
  name: string;
  age: number;
  address: Address;
  hobbies: string[];
  active: boolean;
}

function assertIsAddress(value: unknown): asserts value is Address {
  if (typeof value !== 'object' || value === null) {
    throw new TypeError('Expected address object');
  }
  
  const obj = value as any;
  const requiredFields = ['street', 'city', 'zipCode', 'country'];
  
  for (const field of requiredFields) {
    if (typeof obj[field] !== 'string') {
      throw new TypeError(`Expected string for address.${field}, got ${typeof obj[field]}`);
    }
  }
}

function assertIsPerson(value: unknown): asserts value is Person {
  if (typeof value !== 'object' || value === null) {
    throw new TypeError('Expected person object');
  }
  
  const obj = value as any;
  
  if (typeof obj.name !== 'string') {
    throw new TypeError(`Expected string for name, got ${typeof obj.name}`);
  }
  
  if (typeof obj.age !== 'number') {
    throw new TypeError(`Expected number for age, got ${typeof obj.age}`);
  }
  
  if (typeof obj.active !== 'boolean') {
    throw new TypeError(`Expected boolean for active, got ${typeof obj.active}`);
  }
  
  assertIsAddress(obj.address);
  assertIsStringArray(obj.hobbies);
}

// 🌟 Array assertion with item validation
function assertIsArrayOf<T>(
  value: unknown,
  itemAssertion: (item: unknown) => asserts item is T
): asserts value is T[] {
  assertIsArray(value);
  
  value.forEach((item, index) => {
    try {
      itemAssertion(item);
    } catch (error) {
      throw new Error(`Item at index ${index}: ${error.message}`);
    }
  });
}

function assertIsPersonArray(value: unknown): asserts value is Person[] {
  assertIsArrayOf(value, assertIsPerson);
}

// ✅ Using nested assertions
function processPersonData(data: unknown) {
  assertIsPerson(data);
  // ✨ TypeScript knows data is Person with all nested types
  console.log(`Person: ${data.name}, Age: ${data.age}`);
  console.log(`Lives in ${data.address.city}, ${data.address.country}`);
  console.log(`Hobbies: ${data.hobbies.join(', ')}`);
  console.log(`Status: ${data.active ? 'Active' : 'Inactive'}`);
}

// 🎨 Optional property assertions
interface OptionalProfile {
  id: number;
  name: string;
  bio?: string;
  website?: string;
  socialMedia?: {
    twitter?: string;
    linkedin?: string;
  };
}

function assertIsOptionalProfile(value: unknown): asserts value is OptionalProfile {
  if (typeof value !== 'object' || value === null) {
    throw new TypeError('Expected profile object');
  }
  
  const obj = value as any;
  
  if (typeof obj.id !== 'number') {
    throw new TypeError(`Expected number for id, got ${typeof obj.id}`);
  }
  
  if (typeof obj.name !== 'string') {
    throw new TypeError(`Expected string for name, got ${typeof obj.name}`);
  }
  
  // Optional properties
  if (obj.bio !== undefined && typeof obj.bio !== 'string') {
    throw new TypeError(`Expected string for bio, got ${typeof obj.bio}`);
  }
  
  if (obj.website !== undefined && typeof obj.website !== 'string') {
    throw new TypeError(`Expected string for website, got ${typeof obj.website}`);
  }
  
  if (obj.socialMedia !== undefined) {
    if (typeof obj.socialMedia !== 'object' || obj.socialMedia === null) {
      throw new TypeError('Expected object for socialMedia');
    }
    
    const social = obj.socialMedia;
    if (social.twitter !== undefined && typeof social.twitter !== 'string') {
      throw new TypeError(`Expected string for twitter, got ${typeof social.twitter}`);
    }
    
    if (social.linkedin !== undefined && typeof social.linkedin !== 'string') {
      throw new TypeError(`Expected string for linkedin, got ${typeof social.linkedin}`);
    }
  }
}

🛡️ Assertion Utilities and Patterns

🔍 Assertion Function Library

// 🌟 Comprehensive assertion library
class Assertions {
  // Primitive assertions
  static string(value: unknown, fieldName = 'value'): asserts value is string {
    if (typeof value !== 'string') {
      throw new TypeError(`Expected string for ${fieldName}, got ${typeof value}`);
    }
  }
  
  static number(value: unknown, fieldName = 'value'): asserts value is number {
    if (typeof value !== 'number' || isNaN(value)) {
      throw new TypeError(`Expected number for ${fieldName}, got ${typeof value}`);
    }
  }
  
  static boolean(value: unknown, fieldName = 'value'): asserts value is boolean {
    if (typeof value !== 'boolean') {
      throw new TypeError(`Expected boolean for ${fieldName}, got ${typeof value}`);
    }
  }
  
  static object(value: unknown, fieldName = 'value'): asserts value is Record<string, unknown> {
    if (typeof value !== 'object' || value === null || Array.isArray(value)) {
      throw new TypeError(`Expected object for ${fieldName}, got ${typeof value}`);
    }
  }
  
  // Array assertions
  static array(value: unknown, fieldName = 'value'): asserts value is unknown[] {
    if (!Array.isArray(value)) {
      throw new TypeError(`Expected array for ${fieldName}, got ${typeof value}`);
    }
  }
  
  static arrayOf<T>(
    itemAssertion: (item: unknown, fieldName?: string) => asserts item is T,
    fieldName = 'value'
  ) {
    return (value: unknown): asserts value is T[] => {
      Assertions.array(value, fieldName);
      value.forEach((item, index) => {
        try {
          itemAssertion(item, `${fieldName}[${index}]`);
        } catch (error) {
          throw new Error(`${fieldName}[${index}]: ${error.message}`);
        }
      });
    };
  }
  
  // Object property assertions
  static hasProperty<K extends string>(
    key: K,
    fieldName = 'object'
  ) {
    return (obj: unknown): asserts obj is Record<K, unknown> => {
      Assertions.object(obj, fieldName);
      if (!(key in obj)) {
        throw new Error(`Missing required property '${key}' in ${fieldName}`);
      }
    };
  }
  
  static hasStringProperty<K extends string>(
    key: K,
    fieldName = 'object'
  ) {
    return (obj: unknown): asserts obj is Record<K, string> => {
      const hasPropertyAssertion = Assertions.hasProperty(key, fieldName);
      hasPropertyAssertion(obj);
      Assertions.string((obj as any)[key], `${fieldName}.${key}`);
    };
  }
  
  static hasNumberProperty<K extends string>(
    key: K,
    fieldName = 'object'
  ) {
    return (obj: unknown): asserts obj is Record<K, number> => {
      const hasPropertyAssertion = Assertions.hasProperty(key, fieldName);
      hasPropertyAssertion(obj);
      Assertions.number((obj as any)[key], `${fieldName}.${key}`);
    };
  }
  
  // Union type assertions
  static oneOf<T extends readonly any[]>(
    ...assertions: {
      [K in keyof T]: (value: unknown) => asserts value is T[K]
    }
  ) {
    return (value: unknown): asserts value is T[number] => {
      const errors: string[] = [];
      
      for (const assertion of assertions) {
        try {
          assertion(value);
          return; // If any assertion passes, return
        } catch (error) {
          errors.push(error.message);
        }
      }
      
      throw new Error(`Value failed all union checks: ${errors.join(', ')}`);
    };
  }
  
  // Range and validation assertions
  static inRange(min: number, max: number, fieldName = 'value') {
    return (value: unknown): asserts value is number => {
      Assertions.number(value, fieldName);
      if (value < min || value> max) {
        throw new RangeError(`${fieldName} must be between ${min} and ${max}, got ${value}`);
      }
    };
  }
  
  static minLength(minLen: number, fieldName = 'value') {
    return (value: unknown): asserts value is string => {
      Assertions.string(value, fieldName);
      if (value.length < minLen) {
        throw new Error(`${fieldName} must be at least ${minLen} characters, got ${value.length}`);
      }
    };
  }
  
  static matches(pattern: RegExp, fieldName = 'value') {
    return (value: unknown): asserts value is string => {
      Assertions.string(value, fieldName);
      if (!pattern.test(value)) {
        throw new Error(`${fieldName} does not match required pattern: ${pattern}`);
      }
    };
  }
  
  // Optional assertions
  static optional<T>(assertion: (value: unknown) => asserts value is T) {
    return (value: unknown): asserts value is T | undefined => {
      if (value !== undefined) {
        assertion(value);
      }
    };
  }
  
  static nullable<T>(assertion: (value: unknown) => asserts value is T) {
    return (value: unknown): asserts value is T | null => {
      if (value !== null) {
        assertion(value);
      }
    };
  }
}

// ✅ Using the assertion library
const assertIsUser = (value: unknown): asserts value is User => {
  Assertions.object(value, 'user');
  
  const hasId = Assertions.hasNumberProperty('id', 'user');
  const hasName = Assertions.hasStringProperty('name', 'user');
  const hasEmail = Assertions.hasStringProperty('email', 'user');
  
  hasId(value);
  hasName(value);
  hasEmail(value);
  
  // Additional email validation
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailPattern.test((value as any).email)) {
    throw new Error('Invalid email format');
  }
};

const assertIsStringOrNumber = Assertions.oneOf(
  Assertions.string,
  Assertions.number
);

const assertIsUserArray = Assertions.arrayOf(assertIsUser);

🎯 Schema-Based Assertions

// 🌟 Schema builder for assertions
type AssertionSchema = {
  string: () => (value: unknown) => asserts value is string;
  number: () => (value: unknown) => asserts value is number;
  boolean: () => (value: unknown) => asserts value is boolean;
  array: <T>(itemSchema: AssertionSchema) => (value: unknown) => asserts value is T[];
  object: <T extends Record<string, any>>(
    schema: { [K in keyof T]: AssertionSchema }
  ) => (value: unknown) => asserts value is T;
  optional: <T>(schema: AssertionSchema) => (value: unknown) => asserts value is T | undefined;
  union: <T extends readonly any[]>(
    ...schemas: AssertionSchema[]
  ) => (value: unknown) => asserts value is T[number];
};

function createAssertionSchema(): AssertionSchema {
  return {
    string: () => Assertions.string,
    number: () => Assertions.number,
    boolean: () => Assertions.boolean,
    
    array: <T>(itemSchema: any) => (value: unknown): asserts value is T[] => {
      Assertions.array(value);
      const itemAssertion = compileSchema(itemSchema);
      value.forEach((item, index) => {
        try {
          itemAssertion(item);
        } catch (error) {
          throw new Error(`Array item ${index}: ${error.message}`);
        }
      });
    },
    
    object: <T extends Record<string, any>>(schema: any) => 
      (value: unknown): asserts value is T => {
        Assertions.object(value);
        
        for (const [key, propertySchema] of Object.entries(schema)) {
          const propertyAssertion = compileSchema(propertySchema);
          
          if (!(key in value)) {
            throw new Error(`Missing required property: ${key}`);
          }
          
          try {
            propertyAssertion((value as any)[key]);
          } catch (error) {
            throw new Error(`Property ${key}: ${error.message}`);
          }
        }
      },
    
    optional: <T>(schema: any) => (value: unknown): asserts value is T | undefined => {
      if (value !== undefined) {
        const assertion = compileSchema(schema);
        assertion(value);
      }
    },
    
    union: <T extends readonly any[]>(...schemas: any[]) => 
      (value: unknown): asserts value is T[number] => {
        const errors: string[] = [];
        
        for (const schema of schemas) {
          try {
            const assertion = compileSchema(schema);
            assertion(value);
            return; // If any schema matches, return
          } catch (error) {
            errors.push(error.message);
          }
        }
        
        throw new Error(`No union variant matched: ${errors.join(', ')}`);
      },
  };
}

function compileSchema(schema: any): (value: unknown) => asserts value is any {
  if (typeof schema === 'function') {
    return schema();
  }
  
  // Handle complex schema compilation
  throw new Error('Invalid schema format');
}

// 🎨 Schema usage examples
const schema = createAssertionSchema();

const assertIsPost = schema.object({
  id: schema.number(),
  title: schema.string(),
  content: schema.string(),
  published: schema.boolean(),
  tags: schema.array(schema.string()),
  author: schema.object({
    id: schema.number(),
    name: schema.string(),
    email: schema.string(),
  }),
  publishedAt: schema.optional(schema.string()),
  category: schema.union(
    schema.string(),
    schema.object({
      id: schema.number(),
      name: schema.string(),
    })
  ),
});

function processPost(data: unknown) {
  assertIsPost(data);
  
  // ✨ Full type safety with inferred types
  console.log(`Post: ${data.title} by ${data.author.name}`);
  console.log(`Tags: ${data.tags.join(', ')}`);
  
  if (data.publishedAt) {
    console.log(`Published: ${data.publishedAt}`);
  }
  
  if (typeof data.category === 'string') {
    console.log(`Category: ${data.category}`);
  } else {
    console.log(`Category: ${data.category.name} (ID: ${data.category.id})`);
  }
}

🎮 Real-World Applications

🌐 API Validation with Assertions

// 🎯 API request/response validation
interface APIRequest {
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  path: string;
  headers?: Record<string, string>;
  body?: unknown;
}

interface APIResponse<T> {
  status: number;
  statusText: string;
  data: T;
  headers: Record<string, string>;
}

function assertIsAPIRequest(value: unknown): asserts value is APIRequest {
  Assertions.object(value, 'request');
  
  const obj = value as any;
  
  if (!['GET', 'POST', 'PUT', 'DELETE'].includes(obj.method)) {
    throw new Error(`Invalid HTTP method: ${obj.method}`);
  }
  
  Assertions.string(obj.path, 'path');
  
  if (obj.headers !== undefined) {
    Assertions.object(obj.headers, 'headers');
    for (const [key, val] of Object.entries(obj.headers)) {
      Assertions.string(val, `headers.${key}`);
    }
  }
}

function assertIsAPIResponse<T>(
  value: unknown,
  dataAssertion: (data: unknown) => asserts data is T
): asserts value is APIResponse<T> {
  Assertions.object(value, 'response');
  
  const obj = value as any;
  
  Assertions.number(obj.status, 'status');
  Assertions.string(obj.statusText, 'statusText');
  Assertions.object(obj.headers, 'headers');
  
  dataAssertion(obj.data);
}

// ✨ Safe API client with assertions
class AssertiveAPIClient {
  async request<T>(
    req: APIRequest,
    responseDataAssertion: (data: unknown) => asserts data is T
  ): Promise<APIResponse<T>> {
    // Validate request
    assertIsAPIRequest(req);
    
    try {
      const response = await fetch(req.path, {
        method: req.method,
        headers: req.headers,
        body: req.body ? JSON.stringify(req.body) : undefined,
      });
      
      const data = await response.json();
      const apiResponse = {
        status: response.status,
        statusText: response.statusText,
        data,
        headers: Object.fromEntries(response.headers.entries()),
      };
      
      // Validate response
      assertIsAPIResponse(apiResponse, responseDataAssertion);
      
      return apiResponse;
    } catch (error) {
      if (error instanceof TypeError || error instanceof Error) {
        throw error; // Re-throw assertion errors
      }
      throw new Error(`Network error: ${error}`);
    }
  }
}

// Usage
const client = new AssertiveAPIClient();

async function fetchUser(id: number) {
  const response = await client.request(
    {
      method: 'GET',
      path: `/api/users/${id}`,
      headers: { 'Accept': 'application/json' },
    },
    assertIsUser
  );
  
  // ✨ TypeScript knows response.data is User
  console.log(`User: ${response.data.name} (${response.data.email})`);
  return response.data;
}

🎨 Configuration Validation

// 🌟 Application configuration with assertions
interface DatabaseConfig {
  host: string;
  port: number;
  username: string;
  password: string;
  database: string;
  ssl: boolean;
  maxConnections: number;
}

interface ServerConfig {
  port: number;
  host: string;
  cors: {
    enabled: boolean;
    origins: string[];
  };
  rateLimit: {
    windowMs: number;
    maxRequests: number;
  };
}

interface AppConfig {
  environment: 'development' | 'staging' | 'production';
  database: DatabaseConfig;
  server: ServerConfig;
  features: {
    authentication: boolean;
    logging: boolean;
    metrics: boolean;
  };
  secrets: {
    jwtSecret: string;
    apiKey: string;
  };
}

function assertIsDatabaseConfig(value: unknown): asserts value is DatabaseConfig {
  Assertions.object(value, 'database');
  
  const obj = value as any;
  
  Assertions.string(obj.host, 'database.host');
  Assertions.number(obj.port, 'database.port');
  Assertions.string(obj.username, 'database.username');
  Assertions.string(obj.password, 'database.password');
  Assertions.string(obj.database, 'database.database');
  Assertions.boolean(obj.ssl, 'database.ssl');
  Assertions.number(obj.maxConnections, 'database.maxConnections');
  
  // Additional validations
  if (obj.port < 1 || obj.port> 65535) {
    throw new RangeError('Database port must be between 1 and 65535');
  }
  
  if (obj.maxConnections < 1 || obj.maxConnections> 1000) {
    throw new RangeError('Database maxConnections must be between 1 and 1000');
  }
}

function assertIsServerConfig(value: unknown): asserts value is ServerConfig {
  Assertions.object(value, 'server');
  
  const obj = value as any;
  
  Assertions.number(obj.port, 'server.port');
  Assertions.string(obj.host, 'server.host');
  
  // CORS config
  Assertions.object(obj.cors, 'server.cors');
  Assertions.boolean(obj.cors.enabled, 'server.cors.enabled');
  
  const assertIsStringArray = Assertions.arrayOf(Assertions.string);
  assertIsStringArray(obj.cors.origins);
  
  // Rate limit config
  Assertions.object(obj.rateLimit, 'server.rateLimit');
  Assertions.number(obj.rateLimit.windowMs, 'server.rateLimit.windowMs');
  Assertions.number(obj.rateLimit.maxRequests, 'server.rateLimit.maxRequests');
  
  // Validation
  if (obj.port < 1 || obj.port> 65535) {
    throw new RangeError('Server port must be between 1 and 65535');
  }
}

function assertIsAppConfig(value: unknown): asserts value is AppConfig {
  Assertions.object(value, 'config');
  
  const obj = value as any;
  
  // Environment validation
  if (!['development', 'staging', 'production'].includes(obj.environment)) {
    throw new Error(`Invalid environment: ${obj.environment}`);
  }
  
  // Nested config validation
  assertIsDatabaseConfig(obj.database);
  assertIsServerConfig(obj.server);
  
  // Features validation
  Assertions.object(obj.features, 'features');
  Assertions.boolean(obj.features.authentication, 'features.authentication');
  Assertions.boolean(obj.features.logging, 'features.logging');
  Assertions.boolean(obj.features.metrics, 'features.metrics');
  
  // Secrets validation
  Assertions.object(obj.secrets, 'secrets');
  Assertions.string(obj.secrets.jwtSecret, 'secrets.jwtSecret');
  Assertions.string(obj.secrets.apiKey, 'secrets.apiKey');
  
  // Additional secret validation
  if (obj.secrets.jwtSecret.length < 32) {
    throw new Error('JWT secret must be at least 32 characters long');
  }
}

// ✨ Safe configuration loader
class ConfigurationManager {
  private config: AppConfig | null = null;
  
  loadFromEnvironment(): AppConfig {
    const rawConfig = {
      environment: process.env.NODE_ENV || 'development',
      database: {
        host: process.env.DB_HOST || 'localhost',
        port: parseInt(process.env.DB_PORT || '5432', 10),
        username: process.env.DB_USERNAME || 'user',
        password: process.env.DB_PASSWORD || 'password',
        database: process.env.DB_NAME || 'myapp',
        ssl: process.env.DB_SSL === 'true',
        maxConnections: parseInt(process.env.DB_MAX_CONNECTIONS || '10', 10),
      },
      server: {
        port: parseInt(process.env.PORT || '3000', 10),
        host: process.env.HOST || '0.0.0.0',
        cors: {
          enabled: process.env.CORS_ENABLED !== 'false',
          origins: (process.env.CORS_ORIGINS || '*').split(','),
        },
        rateLimit: {
          windowMs: parseInt(process.env.RATE_LIMIT_WINDOW || '900000', 10),
          maxRequests: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
        },
      },
      features: {
        authentication: process.env.FEATURE_AUTH !== 'false',
        logging: process.env.FEATURE_LOGGING !== 'false',
        metrics: process.env.FEATURE_METRICS !== 'false',
      },
      secrets: {
        jwtSecret: process.env.JWT_SECRET || '',
        apiKey: process.env.API_KEY || '',
      },
    };
    
    // Validate the configuration
    assertIsAppConfig(rawConfig);
    
    this.config = rawConfig;
    return rawConfig;
  }
  
  getConfig(): AppConfig {
    if (!this.config) {
      throw new Error('Configuration not loaded. Call loadFromEnvironment() first.');
    }
    return this.config;
  }
}

// Usage
const configManager = new ConfigurationManager();

try {
  const config = configManager.loadFromEnvironment();
  console.log(`Starting ${config.environment} server on ${config.server.host}:${config.server.port}`);
  console.log(`Database: ${config.database.host}:${config.database.port}/${config.database.database}`);
} catch (error) {
  console.error('Configuration validation failed:', error.message);
  process.exit(1);
}

🎓 Key Takeaways

You’ve mastered the assertive power of TypeScript! Here’s what you now command:

  • Custom assertion functions with automatic type narrowing and error throwing 💪
  • Discriminated union assertions for robust state management 🛡️
  • Complex object validation with detailed error messages 🎯
  • Assertion libraries and utilities for reusable validation logic 🐛
  • Schema-based validation with compile-time type inference 🚀
  • Real-world applications in APIs, configuration, and data validation ✨
  • Error-first design that fails fast and provides clear diagnostics 🔄

Remember: Assertion functions bridge the gap between runtime validation and compile-time type safety! 🤝

🤝 Next Steps

Congratulations! 🎉 You’ve become a master of assertion functions and type safety!

Here’s what to do next:

  1. 💻 Build robust validation layers with custom assertion functions
  2. 🏗️ Create comprehensive error handling systems with detailed messages
  3. 📚 Move on to our next tutorial: Control Flow Analysis - How TypeScript Tracks Types
  4. 🌟 Implement schema validation libraries with assertion-based APIs
  5. 🔍 Explore advanced error recovery and validation patterns
  6. 🎯 Build fail-fast systems with intelligent error reporting
  7. 🚀 Push the boundaries of runtime safety with compile-time guarantees

Remember: You now possess the power to create systems that fail fast, fail clearly, and provide excellent type safety! Use this knowledge to build incredibly robust and maintainable applications. 🚀


Happy asserting! 🎉⚡✨