io-ts
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);