io-ts

Validation LibraryTypeScriptRuntime Type CheckingFunctional ProgrammingDecoderEncoder

Library

io-ts

Overview

io-ts is a TypeScript-exclusive runtime type system library that adopts a functional programming approach specifically designed for "IO (input/output) decoding/encoding". It complements TypeScript's limitation of providing only static type checking by ensuring type safety at runtime. Through deep integration with the fp-ts library, it leverages monadic constructs like Either and Option to handle error processing and type conversion in an integrated manner. As of 2025, it continues to play a crucial role in projects requiring advanced type safety through the combination of functional programming and TypeScript.

Details

The io-ts 2.2 series is the latest stable version as of 2025, built on the foundation of TypeScript's type system and fp-ts's rich abstractions for functional programming. The codec system realizes a "single source of truth" that can generate both static types and runtime validators from a single definition. It provides composable error handling through Either monads, advanced type abstraction via HKT (Higher Kinded Types), and extensibility through the Schemable pattern, supporting type-safe application development following functional programming principles.

Key Features

  • Runtime Type System: Runtime type checking and validation
  • Codec-Based Design: Type definitions integrating decode/encode functionality
  • fp-ts Integration: Functional error handling through monads like Either and Option
  • Single Source of Truth: Generate both static types and runtime validators from one definition
  • Advanced Composability: Ability to compose complex types from small codecs
  • Schemable Pattern: Extensible type class-based design

Pros and Cons

Pros

  • Extends TypeScript's type safety to runtime
  • Complete integration with fp-ts ecosystem providing powerful abstractions
  • Design based on functional programming principles
  • Eliminates type definition duplication (unifies static types and runtime validators)
  • Detailed and customizable error information
  • High composability and extensibility

Cons

  • High learning curve requiring fp-ts knowledge
  • Complex for developers unfamiliar with functional programming
  • Either monad-based error handling can become verbose
  • More verbose than other validation libraries
  • Smaller community size compared to Zod or Yup
  • Runtime performance overhead due to implementation complexity

Reference Pages

Code Examples

Installation and Basic Setup

# Installing io-ts
npm install io-ts
yarn add io-ts
pnpm add io-ts

# fp-ts is also required (mandatory dependency)
npm install fp-ts

# TypeScript configuration required
# TypeScript 4.7 or higher is recommended

Basic Codec Definition and Validation

import * as t from 'io-ts';
import { isLeft, isRight } from 'fp-ts/Either';
import { PathReporter } from 'io-ts/PathReporter';

// Basic codec definition
const User = t.type({
  id: t.number,
  name: t.string,
  email: t.string,
  age: t.number,
  isActive: t.boolean
});

// Static type extraction (TypeScript compile time)
type User = t.TypeOf<typeof User>;
// Result: { id: number; name: string; email: string; age: number; isActive: boolean; }

// Runtime validation execution
const userData = {
  id: 1,
  name: 'John Doe',
  email: '[email protected]',
  age: 30,
  isActive: true
};

// Success case
const result = User.decode(userData);
if (isRight(result)) {
  console.log('Validation successful:', result.right);
  // result.right can be safely used as User type
} else {
  console.log('Validation failed:', PathReporter.report(result));
}

// Failure case
const invalidData = {
  id: 'invalid', // string instead of number
  name: 'Jane Smith',
  email: '[email protected]',
  age: '25', // string instead of number
  isActive: 'true' // string instead of boolean
};

const invalidResult = User.decode(invalidData);
if (isLeft(invalidResult)) {
  console.log('Validation errors:');
  PathReporter.report(invalidResult).forEach(error => {
    console.log(`  ${error}`);
  });
}

// Optional fields and default values
const UserWithOptional = t.type({
  id: t.number,
  name: t.string,
  profile: t.union([
    t.type({
      bio: t.string,
      website: t.union([t.string, t.null])
    }),
    t.undefined
  ])
});

// Partial object (all fields optional)
const PartialUser = t.partial({
  name: t.string,
  email: t.string,
  age: t.number
});

type PartialUser = t.TypeOf<typeof PartialUser>;
// Result: { name?: string; email?: string; age?: number; }

// Primitive type validation
const stringCodec = t.string;
const numberCodec = t.number;
const booleanCodec = t.boolean;
const nullCodec = t.null;
const undefinedCodec = t.undefined;

console.log(isRight(stringCodec.decode('hello'))); // true
console.log(isRight(numberCodec.decode(42))); // true
console.log(isRight(booleanCodec.decode(true))); // true

Arrays, Records, and Complex Structure Validation

import * as t from 'io-ts';
import { pipe } from 'fp-ts/function';
import { fold } from 'fp-ts/Either';

// Array codecs
const NumberArray = t.array(t.number);
const StringArray = t.array(t.string);

type NumberArray = t.TypeOf<typeof NumberArray>; // number[]
type StringArray = t.TypeOf<typeof StringArray>; // string[]

// Record (dictionary) codecs
const StringRecord = t.record(t.string);
const NumberRecord = t.record(t.number);

type StringRecord = t.TypeOf<typeof StringRecord>; // Record<string, string>
type NumberRecord = t.TypeOf<typeof NumberRecord>; // Record<string, number>

// Nested object definitions
const Address = t.type({
  street: t.string,
  city: t.string,
  postalCode: t.string,
  country: t.string
});

const Company = t.type({
  name: t.string,
  industry: t.string,
  employees: t.number
});

const ComplexUser = t.type({
  id: t.number,
  personalInfo: t.type({
    firstName: t.string,
    lastName: t.string,
    birthDate: t.string // ISO date string
  }),
  contactInfo: t.type({
    email: t.string,
    phone: t.union([t.string, t.null])
  }),
  address: Address,
  company: t.union([Company, t.null]),
  tags: t.array(t.string),
  preferences: t.record(t.union([t.string, t.number, t.boolean])),
  metadata: t.unknown // arbitrary JSON data
});

// Complex data validation
const complexData = {
  id: 1,
  personalInfo: {
    firstName: 'John',
    lastName: 'Doe',
    birthDate: '1990-05-15'
  },
  contactInfo: {
    email: '[email protected]',
    phone: '+1-555-123-4567'
  },
  address: {
    street: '123 Main Street',
    city: 'New York',
    postalCode: '10001',
    country: 'USA'
  },
  company: {
    name: 'Example Corp',
    industry: 'Technology',
    employees: 100
  },
  tags: ['developer', 'typescript', 'javascript'],
  preferences: {
    theme: 'dark',
    notifications: true,
    maxItems: 50
  },
  metadata: {
    source: 'manual_entry',
    version: 1,
    additional: { 
      notes: 'Test user data'
    }
  }
};

// Error handling with Either monad
const validateComplexUser = (data: unknown) => {
  return pipe(
    ComplexUser.decode(data),
    fold(
      // Error handling (Left case)
      (errors) => {
        console.log('Validation errors:');
        PathReporter.report({ _tag: 'Left', left: errors }).forEach(error => {
          console.log(`  ${error}`);
        });
        return null;
      },
      // Success handling (Right case)
      (validUser) => {
        console.log('Validation successful');
        console.log(`User: ${validUser.personalInfo.firstName} ${validUser.personalInfo.lastName}`);
        console.log(`Company: ${validUser.company?.name || 'None'}`);
        console.log(`Tags count: ${validUser.tags.length}`);
        return validUser;
      }
    )
  );
};

validateComplexUser(complexData);

// Tuple (fixed-length array) validation
const Coordinate = t.tuple([t.number, t.number]);
const NamedCoordinate = t.tuple([t.string, t.number, t.number]);

type Coordinate = t.TypeOf<typeof Coordinate>; // [number, number]
type NamedCoordinate = t.TypeOf<typeof NamedCoordinate>; // [string, number, number]

const coordResult = Coordinate.decode([40.7128, -74.0060]);
console.log('Coordinate validation:', isRight(coordResult));

// Union types (one of several types)
const Status = t.union([
  t.literal('pending'),
  t.literal('approved'),
  t.literal('rejected')
]);

const NumberOrString = t.union([t.number, t.string]);
const OptionalString = t.union([t.string, t.null, t.undefined]);

type Status = t.TypeOf<typeof Status>; // 'pending' | 'approved' | 'rejected'
type NumberOrString = t.TypeOf<typeof NumberOrString>; // number | string

Custom Codecs and Advanced Validation

import * as t from 'io-ts';
import { pipe } from 'fp-ts/function';
import { chain, map } from 'fp-ts/Either';

// Brand types for custom validation
interface EmailBrand {
  readonly Email: unique symbol;
}
type Email = string & EmailBrand;

interface PositiveNumberBrand {
  readonly PositiveNumber: unique symbol;
}
type PositiveNumber = number & PositiveNumberBrand;

// Custom codec: email validation
const EmailCodec = new t.Type<Email, string, unknown>(
  'Email',
  (input): input is Email => 
    typeof input === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input),
  (input, context) => {
    if (typeof input !== 'string') {
      return t.failure(input, context, 'Expected string');
    }
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) {
      return t.failure(input, context, 'Invalid email format');
    }
    return t.success(input as Email);
  },
  t.identity
);

// Custom codec: positive number validation
const PositiveNumberCodec = new t.Type<PositiveNumber, number, unknown>(
  'PositiveNumber',
  (input): input is PositiveNumber => 
    typeof input === 'number' && input > 0,
  (input, context) => {
    if (typeof input !== 'number') {
      return t.failure(input, context, 'Expected number');
    }
    if (input <= 0) {
      return t.failure(input, context, 'Expected positive number');
    }
    return t.success(input as PositiveNumber);
  },
  t.identity
);

// Date string custom codec
interface DateStringBrand {
  readonly DateString: unique symbol;
}
type DateString = string & DateStringBrand;

const DateStringCodec = new t.Type<DateString, string, unknown>(
  'DateString',
  (input): input is DateString => 
    typeof input === 'string' && !isNaN(Date.parse(input)),
  (input, context) => {
    if (typeof input !== 'string') {
      return t.failure(input, context, 'Expected string');
    }
    if (isNaN(Date.parse(input))) {
      return t.failure(input, context, 'Invalid date string');
    }
    return t.success(input as DateString);
  },
  t.identity
);

// Type definition using custom codecs
const ValidatedUser = t.type({
  id: PositiveNumberCodec,
  name: t.string,
  email: EmailCodec,
  registeredAt: DateStringCodec,
  age: PositiveNumberCodec
});

// Usage example
const validateUserData = (data: unknown) => {
  const result = ValidatedUser.decode(data);
  
  if (isRight(result)) {
    const user = result.right;
    console.log('User validation successful:');
    console.log(`ID: ${user.id}`);
    console.log(`Name: ${user.name}`);
    console.log(`Email: ${user.email}`); // Email brand type for type safety
    console.log(`Registered: ${user.registeredAt}`); // DateString brand type
    console.log(`Age: ${user.age}`); // PositiveNumber brand type
    return user;
  } else {
    console.log('User validation failed:');
    PathReporter.report(result).forEach(error => console.log(`  ${error}`));
    return null;
  }
};

// Test data
const validUserData = {
  id: 1,
  name: 'John Doe',
  email: '[email protected]',
  registeredAt: '2025-01-01T00:00:00Z',
  age: 30
};

const invalidUserData = {
  id: -1, // negative number
  name: 'Jane Smith',
  email: 'invalid-email', // invalid email format
  registeredAt: 'not-a-date', // invalid date
  age: 0 // not positive
};

console.log('=== Valid Data Test ===');
validateUserData(validUserData);

console.log('\n=== Invalid Data Test ===');
validateUserData(invalidUserData);

// Recursive type definition (self-referencing type)
interface Category {
  id: number;
  name: string;
  parent?: Category;
  children: Category[];
}

const CategoryCodec: t.Type<Category> = t.recursion('Category', () =>
  t.type({
    id: t.number,
    name: t.string,
    parent: t.union([
      t.recursion('Category', () => CategoryCodec),
      t.undefined
    ]),
    children: t.array(t.recursion('Category', () => CategoryCodec))
  })
);

// Conditional validation
const ConditionalSchema = t.type({
  type: t.union([t.literal('user'), t.literal('admin')]),
  name: t.string,
  permissions: t.union([t.array(t.string), t.undefined])
});

// Custom validation: admin users must have permissions
const validateConditionalData = (data: unknown) => {
  return pipe(
    ConditionalSchema.decode(data),
    chain((decoded) => {
      if (decoded.type === 'admin' && (!decoded.permissions || decoded.permissions.length === 0)) {
        return t.failure(data, [], 'Admin users must have permissions');
      }
      return t.success(decoded);
    })
  );
};

Either Monad and Functional Error Handling

import * as t from 'io-ts';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
import { sequenceT } from 'fp-ts/Apply';

// Composing multiple validation results
const UserInput = t.type({
  name: t.string,
  age: t.number,
  email: t.string
});

const CompanyInput = t.type({
  name: t.string,
  industry: t.string
});

// Parallel validation of multiple codecs
const validateMultipleInputs = (userData: unknown, companyData: unknown) => {
  const userResult = UserInput.decode(userData);
  const companyResult = CompanyInput.decode(companyData);

  // Use sequenceT to compose multiple Eithers
  return pipe(
    sequenceT(E.Apply)(userResult, companyResult),
    E.map(([user, company]) => ({
      user,
      company,
      combined: {
        userName: user.name,
        companyName: company.name,
        relationship: `${user.name} works at ${company.name}`
      }
    }))
  );
};

// Custom error reporter
interface CustomError {
  field: string;
  message: string;
  value: unknown;
}

const createCustomErrorReporter = (validation: t.Validation<any>): CustomError[] => {
  if (E.isRight(validation)) return [];

  const errors: CustomError[] = [];
  
  const processError = (error: t.ValidationError): void => {
    const field = error.context.map(c => c.key).filter(Boolean).join('.');
    const message = error.message || `Invalid value at ${field}`;
    
    errors.push({
      field: field || 'root',
      message,
      value: error.value
    });
  };

  validation.left.forEach(processError);
  return errors;
};

// Functional style validation processing
const processUserRegistration = (userData: unknown) => {
  return pipe(
    UserInput.decode(userData),
    E.mapLeft(createCustomErrorReporter), // Convert errors to custom format
    E.chain((user) => {
      // Additional business logic validation
      if (user.age < 18) {
        return E.left([{
          field: 'age',
          message: 'Must be at least 18 years old',
          value: user.age
        }]);
      }
      return E.right(user);
    }),
    E.map((user) => {
      // Success processing (e.g., database save)
      return {
        ...user,
        id: Math.random(), // Mock ID generation
        registeredAt: new Date().toISOString(),
        status: 'active' as const
      };
    }),
    E.fold(
      // Error handling
      (errors) => {
        console.log('User registration failed:');
        errors.forEach(error => {
          console.log(`  ${error.field}: ${error.message} (value: ${error.value})`);
        });
        return null;
      },
      // Success processing
      (registeredUser) => {
        console.log('User registration successful:', registeredUser);
        return registeredUser;
      }
    )
  );
};

// Usage examples
const testUserData = {
  name: 'John Doe',
  age: 25,
  email: '[email protected]'
};

const testCompanyData = {
  name: 'Example Corp',
  industry: 'Technology'
};

console.log('=== Multiple Input Validation ===');
const multiResult = validateMultipleInputs(testUserData, testCompanyData);
if (E.isRight(multiResult)) {
  console.log('Both validations successful:', multiResult.right.combined);
} else {
  console.log('Validation failed');
}

console.log('\n=== User Registration Processing ===');
processUserRegistration(testUserData);

console.log('\n=== Age Restriction Error Test ===');
processUserRegistration({ ...testUserData, age: 16 });

API Integration and Data Transformation Patterns

import * as t from 'io-ts';
import * as E from 'fp-ts/Either';
import * as TE from 'fp-ts/TaskEither';
import { pipe } from 'fp-ts/function';

// API response codec
const APIUser = t.type({
  user_id: t.number,
  full_name: t.string,
  email_address: t.string,
  birth_date: t.string,
  is_active: t.boolean,
  created_at: t.string,
  profile: t.partial({
    bio: t.string,
    website: t.string,
    avatar_url: t.string
  })
});

// Internal application type
const AppUser = t.type({
  id: t.number,
  name: t.string,
  email: t.string,
  birthDate: t.string,
  isActive: t.boolean,
  createdAt: t.string,
  profile: t.partial({
    bio: t.string,
    website: t.string,
    avatarUrl: t.string
  })
});

type APIUser = t.TypeOf<typeof APIUser>;
type AppUser = t.TypeOf<typeof AppUser>;

// Convert API response to internal format
const convertAPIUserToAppUser = (apiUser: APIUser): AppUser => ({
  id: apiUser.user_id,
  name: apiUser.full_name,
  email: apiUser.email_address,
  birthDate: apiUser.birth_date,
  isActive: apiUser.is_active,
  createdAt: apiUser.created_at,
  profile: {
    bio: apiUser.profile?.bio,
    website: apiUser.profile?.website,
    avatarUrl: apiUser.profile?.avatar_url
  }
});

// Convert internal format to API format
const convertAppUserToAPIUser = (appUser: AppUser): APIUser => ({
  user_id: appUser.id,
  full_name: appUser.name,
  email_address: appUser.email,
  birth_date: appUser.birthDate,
  is_active: appUser.isActive,
  created_at: appUser.createdAt,
  profile: {
    bio: appUser.profile?.bio,
    website: appUser.profile?.website,
    avatar_url: appUser.profile?.avatarUrl
  }
});

// Asynchronous validation using TaskEither
const fetchAndValidateUser = (userId: number): TE.TaskEither<string, AppUser> => {
  return pipe(
    TE.tryCatch(
      // API call
      async () => {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        return response.json();
      },
      (error) => `API call failed: ${error}`
    ),
    TE.chainEitherK((data) => 
      pipe(
        APIUser.decode(data),
        E.mapLeft((errors) => 
          `Validation failed: ${PathReporter.report(E.left(errors)).join(', ')}`
        )
      )
    ),
    TE.map(convertAPIUserToAppUser)
  );
};

// Batch processing with type safety
const processBatchUsers = async (userIds: number[]) => {
  const results = await Promise.all(
    userIds.map(id => fetchAndValidateUser(id)())
  );

  const { successes, errors } = results.reduce(
    (acc, result, index) => {
      if (E.isRight(result)) {
        acc.successes.push(result.right);
      } else {
        acc.errors.push({ userId: userIds[index], error: result.left });
      }
      return acc;
    },
    { successes: [] as AppUser[], errors: [] as { userId: number; error: string }[] }
  );

  return { successes, errors };
};

// Form data validation and sanitization
const UserFormData = t.type({
  name: t.string,
  email: t.string,
  age: t.string, // comes as string from forms
  bio: t.union([t.string, t.undefined])
});

const sanitizeAndValidateFormData = (formData: unknown) => {
  return pipe(
    UserFormData.decode(formData),
    E.chain((data) => {
      // Sanitization processing
      const sanitized = {
        name: data.name.trim(),
        email: data.email.toLowerCase().trim(),
        age: parseInt(data.age, 10),
        bio: data.bio?.trim()
      };

      // Re-validation after conversion
      const SanitizedUser = t.type({
        name: t.string,
        email: t.string,
        age: t.number,
        bio: t.union([t.string, t.undefined])
      });

      return pipe(
        SanitizedUser.decode(sanitized),
        E.mapLeft(() => 'Post-sanitization validation failed')
      );
    }),
    E.chain((data) => {
      // Business rule validation
      if (data.name.length < 2) {
        return E.left('Name must be at least 2 characters');
      }
      if (data.age < 18 || data.age > 120) {
        return E.left('Age must be between 18 and 120');
      }
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
        return E.left('Please enter a valid email address');
      }
      return E.right(data);
    })
  );
};

// Practical example: Express.js middleware
const createValidationMiddleware = <T>(codec: t.Type<T>) => {
  return (req: any, res: any, next: any) => {
    const result = codec.decode(req.body);
    
    if (E.isLeft(result)) {
      return res.status(400).json({
        error: 'Validation failed',
        details: PathReporter.report(result)
      });
    }
    
    req.validatedBody = result.right;
    next();
  };
};

// Usage example
console.log('=== Form Data Validation ===');
const formData = {
  name: '  John Doe  ',
  email: '[email protected]',
  age: '30',
  bio: '  Software engineer  '
};

const formResult = sanitizeAndValidateFormData(formData);
if (E.isRight(formResult)) {
  console.log('Form validation successful:', formResult.right);
} else {
  console.log('Form validation failed:', formResult.left);
}

Advanced Schema Composition and Metaprogramming

import * as t from 'io-ts';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';

// Dynamic schema composition
const createEntitySchema = <T extends Record<string, t.Type<any>>>(
  name: string,
  fields: T
) => {
  return t.type({
    id: t.number,
    createdAt: t.string,
    updatedAt: t.string,
    ...fields
  }, name);
};

// Basic entity schema creation
const UserSchema = createEntitySchema('User', {
  name: t.string,
  email: t.string,
  age: t.number
});

const PostSchema = createEntitySchema('Post', {
  title: t.string,
  content: t.string,
  authorId: t.number,
  published: t.boolean
});

// Pagination schema
const createPaginatedSchema = <T>(itemCodec: t.Type<T>) => {
  return t.type({
    items: t.array(itemCodec),
    pagination: t.type({
      page: t.number,
      perPage: t.number,
      total: t.number,
      totalPages: t.number
    }),
    metadata: t.partial({
      filters: t.record(t.string),
      sort: t.string,
      search: t.string
    })
  });
};

const PaginatedUsers = createPaginatedSchema(UserSchema);
const PaginatedPosts = createPaginatedSchema(PostSchema);

// Response wrapper schema
const createAPIResponseSchema = <T>(dataCodec: t.Type<T>) => {
  return t.type({
    success: t.boolean,
    data: dataCodec,
    message: t.union([t.string, t.null]),
    timestamp: t.string,
    requestId: t.string
  });
};

const UserAPIResponse = createAPIResponseSchema(UserSchema);
const UsersListAPIResponse = createAPIResponseSchema(PaginatedUsers);

// Conditional schemas (tagged unions)
const NotificationSchema = t.union([
  t.type({
    type: t.literal('email'),
    recipient: t.string, // email address
    subject: t.string,
    body: t.string,
    priority: t.union([t.literal('low'), t.literal('normal'), t.literal('high')])
  }),
  t.type({
    type: t.literal('sms'),
    phoneNumber: t.string,
    message: t.string,
    urgency: t.boolean
  }),
  t.type({
    type: t.literal('push'),
    deviceId: t.string,
    title: t.string,
    body: t.string,
    badge: t.union([t.number, t.null]),
    data: t.record(t.unknown)
  }),
  t.type({
    type: t.literal('webhook'),
    url: t.string,
    method: t.union([t.literal('POST'), t.literal('PUT')]),
    headers: t.record(t.string),
    payload: t.unknown
  })
]);

// Type narrowing functions
const isEmailNotification = (notification: t.TypeOf<typeof NotificationSchema>) => {
  return notification.type === 'email';
};

const isSMSNotification = (notification: t.TypeOf<typeof NotificationSchema>) => {
  return notification.type === 'sms';
};

// Processing based on validation results
const processNotification = (data: unknown) => {
  return pipe(
    NotificationSchema.decode(data),
    E.map((notification) => {
      switch (notification.type) {
        case 'email':
          return {
            channel: 'email',
            recipient: notification.recipient,
            content: `${notification.subject}: ${notification.body}`,
            metadata: { priority: notification.priority }
          };
        case 'sms':
          return {
            channel: 'sms',
            recipient: notification.phoneNumber,
            content: notification.message,
            metadata: { urgent: notification.urgency }
          };
        case 'push':
          return {
            channel: 'push',
            recipient: notification.deviceId,
            content: `${notification.title}: ${notification.body}`,
            metadata: { badge: notification.badge, data: notification.data }
          };
        case 'webhook':
          return {
            channel: 'webhook',
            recipient: notification.url,
            content: JSON.stringify(notification.payload),
            metadata: { method: notification.method, headers: notification.headers }
          };
      }
    })
  );
};

// Intersection (intersection types) for extensible schemas
const TimestampMixin = t.type({
  createdAt: t.string,
  updatedAt: t.string
});

const AuditMixin = t.type({
  createdBy: t.number,
  updatedBy: t.number,
  version: t.number
});

const SoftDeleteMixin = t.type({
  deletedAt: t.union([t.string, t.null]),
  deletedBy: t.union([t.number, t.null])
});

// Complete entity combining mixins
const createFullEntitySchema = <T extends Record<string, t.Type<any>>>(
  name: string,
  fields: T
) => {
  return t.intersection([
    t.type({
      id: t.number,
      ...fields
    }),
    TimestampMixin,
    AuditMixin,
    SoftDeleteMixin
  ], name);
};

const FullUserSchema = createFullEntitySchema('FullUser', {
  name: t.string,
  email: t.string,
  role: t.union([t.literal('user'), t.literal('admin'), t.literal('moderator')])
});

// Type extraction and type guards
type FullUser = t.TypeOf<typeof FullUserSchema>;

const isActiveUser = (user: FullUser): boolean => {
  return user.deletedAt === null;
};

const isAdminUser = (user: FullUser): boolean => {
  return user.role === 'admin' && isActiveUser(user);
};

// Dynamic validation functionality
const validateEntityBatch = <T>(
  codec: t.Type<T>,
  data: unknown[]
): { valid: T[]; invalid: { index: number; errors: string[] }[] } => {
  const valid: T[] = [];
  const invalid: { index: number; errors: string[] }[] = [];

  data.forEach((item, index) => {
    const result = codec.decode(item);
    if (E.isRight(result)) {
      valid.push(result.right);
    } else {
      invalid.push({
        index,
        errors: PathReporter.report(result)
      });
    }
  });

  return { valid, invalid };
};

// Practical examples
console.log('=== Notification Processing Test ===');
const emailNotification = {
  type: 'email',
  recipient: '[email protected]',
  subject: 'Welcome!',
  body: 'Welcome to our service',
  priority: 'normal'
};

const smsNotification = {
  type: 'sms',
  phoneNumber: '+1-555-123-4567',
  message: 'Your verification code is 123456',
  urgency: true
};

[emailNotification, smsNotification].forEach((notification, index) => {
  const result = processNotification(notification);
  if (E.isRight(result)) {
    console.log(`Notification ${index + 1} processing successful:`, result.right);
  } else {
    console.log(`Notification ${index + 1} processing failed:`, PathReporter.report(result));
  }
});

console.log('\n=== Batch Validation Test ===');
const userData = [
  {
    id: 1,
    name: 'John Doe',
    email: '[email protected]',
    role: 'user',
    createdAt: '2025-01-01T00:00:00Z',
    updatedAt: '2025-01-01T00:00:00Z',
    createdBy: 1,
    updatedBy: 1,
    version: 1,
    deletedAt: null,
    deletedBy: null
  },
  {
    id: 'invalid', // invalid ID
    name: 'Jane Smith',
    email: '[email protected]',
    role: 'admin'
    // missing required fields
  }
];

const batchResult = validateEntityBatch(FullUserSchema, userData);
console.log('Valid user count:', batchResult.valid.length);
console.log('Invalid data:', batchResult.invalid);