@bpits/type-guards
v1.1.2
Published
A TypeScript library for building robust, type-safe runtime validators with compile-time property validation enforcement and comprehensive debugging capabilities.
Readme
@bpits/type-guards
A TypeScript library that provides a fluent, type-safe way to build runtime type guards with compile-time validation and helpful debugging features.
Table of Contents
- Installation
- Quick Start
- Why Use This Library?
- Basic Usage
- Built-in Type Guards
- Advanced Usage
- TypeGuardBuilder vs StrictTypeGuardBuilder
- Debugging and Logging
- Real-World Examples
- Best Practices
- TypeScript Configuration
- Contributing
- License
Installation
npm install @bpits/type-guardsQuick Start
import { StrictTypeGuardBuilder, CommonTypeGuards } from '@bpits/type-guards';
interface User {
id: string;
name: string;
email: string;
}
const isUser = StrictTypeGuardBuilder
.start<User>('User')
.validateProperty('id', CommonTypeGuards.basics.string())
.validateProperty('name', CommonTypeGuards.basics.string())
.validateProperty('email', CommonTypeGuards.basics.string())
.build();
// Usage
const data: unknown = { id: "123", name: "John", email: "[email protected]" };
if (isUser(data)) {
// data is now typed as User
console.log(data.name); // TypeScript knows this is safe
}Why Use This Library?
The Problem
When working with external data (APIs, user input, file parsing), we often face a choice between type safety and development speed. Traditional type guards become verbose and error-prone:
// Traditional approach - verbose and error-prone
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object'
&& obj !== null
&& typeof (obj as User).id === 'string' // Easy to make mistakes
&& typeof (obj as User).name === 'string'
&& typeof (obj as User).email === 'string'
// Forgot to validate age? Compiler won't help you
);
}The Solution
This library provides:
- Compile-time safety: TypeScript ensures you validate all properties
- Runtime debugging: Helpful console warnings when validation fails
- Fluent API: Easy to read and maintain
- Reusable validators: Common type guards included
- Flexible validation: Support for complex nested structures
Basic Usage
Simple Object Validation
import { StrictTypeGuardBuilder, CommonTypeGuards } from '@bpits/type-guards';
interface Person {
name: string;
age: number;
}
const isPerson = StrictTypeGuardBuilder
.start<Person>('Person')
.validateProperty('name', CommonTypeGuards.basics.string())
.validateProperty('age', CommonTypeGuards.basics.number())
.build();
// Usage
const data: unknown = JSON.parse('{"name": "Alice", "age": 30}');
if (isPerson(data)) {
console.log(`${data.name} is ${data.age} years old`);
}Nullable Types
interface UserProfile {
username: string;
bio: string | null;
avatar: string | undefined;
}
const isUserProfile = StrictTypeGuardBuilder
.start<UserProfile>('UserProfile')
.validateProperty('username', CommonTypeGuards.basics.string())
.validateProperty('bio', CommonTypeGuards.basics.string().nullable(null))
.validateProperty('avatar', CommonTypeGuards.basics.string().nullable(undefined))
.build();Optional Properties and Ignoring Fields
Sometimes you want to ignore certain properties (perhaps they're computed or internal):
interface ApiResponse {
data: string;
timestamp: number;
_internal: any; // We don't care about this field
}
const isApiResponse = StrictTypeGuardBuilder
.start<ApiResponse>('ApiResponse')
.validateProperty('data', CommonTypeGuards.basics.string())
.validateProperty('timestamp', CommonTypeGuards.basics.number())
.ignoreProperty('_internal') // No validation, no warnings
.build();Built-in Type Guards
Basic Types
CommonTypeGuards.basics.string() // string
CommonTypeGuards.basics.number() // number
CommonTypeGuards.basics.boolean() // boolean
CommonTypeGuards.basics.object() // object
// Nullable versions using fluent interface
CommonTypeGuards.basics.string().nullable() // string | null | undefined
CommonTypeGuards.basics.string().nullable(null) // string | null
CommonTypeGuards.basics.number().nullable() // number | null | undefined
CommonTypeGuards.basics.boolean().nullable() // boolean | null | undefined
CommonTypeGuards.basics.object().nullable() // object | null | undefinedDate Validation
CommonTypeGuards.date.date() // Date object
CommonTypeGuards.date.dateString() // Valid date string
// Nullable versions
CommonTypeGuards.date.date().nullable() // Date | null | undefined
CommonTypeGuards.date.dateString().nullable() // Valid date string | null | undefined
// Usage
interface Event {
name: string;
date: Date;
created: string; // ISO date string
cancelled?: Date; // Optional cancellation date
}
const isEvent = StrictTypeGuardBuilder
.start<Event>('Event')
.validateProperty('name', CommonTypeGuards.basics.string())
.validateProperty('date', CommonTypeGuards.date.date())
.validateProperty('created', CommonTypeGuards.date.dateString())
.validateProperty('cancelled', CommonTypeGuards.date.date().nullable(undefined))
.build();Array Validation
CommonTypeGuards.array.array() // Array<unknown>
CommonTypeGuards.array.arrayOf(typeGuard) // Array<T>
// Nullable versions
CommonTypeGuards.array.array().nullable() // Array<unknown> | null | undefined
CommonTypeGuards.array.arrayOf(typeGuard).nullable() // Array<T> | null | undefined
// Usage
interface TodoList {
items: string[];
priorities: number[];
archived?: string[]; // Optional archived items
}
const isTodoList = StrictTypeGuardBuilder
.start<TodoList>('TodoList')
.validateProperty('items', CommonTypeGuards.array.arrayOf(CommonTypeGuards.basics.string()))
.validateProperty('priorities', CommonTypeGuards.array.arrayOf(CommonTypeGuards.basics.number()))
.validateProperty('archived', CommonTypeGuards.array.arrayOf(CommonTypeGuards.basics.string()).nullable(undefined))
.build();Enum Validation
CommonTypeGuards.enums.memberOf(enumObject) // T[keyof T] - validates enum values
CommonTypeGuards.enums.keyOf(enumObject) // keyof T - validates enum keys
// Nullable versions
CommonTypeGuards.enums.memberOf(enumObject).nullable() // T[keyof T] | null | undefined
CommonTypeGuards.enums.memberOf(enumObject).nullable(null) // T[keyof T] | null
CommonTypeGuards.enums.keyOf(enumObject).nullable() // keyof T | null | undefined
CommonTypeGuards.enums.keyOf(enumObject).nullable(null) // keyof T | null
// Usage with string enum
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending'
}
// Usage with numeric enum
enum Priority {
Low = 1,
Medium = 2,
High = 3
}
// Usage with plain object (acts like enum)
const Colors = {
Red: 'red',
Green: 'green',
Blue: 'blue'
} as const;
interface Task {
title: string;
status: Status; // enum value
priority: Priority; // enum value
statusKey?: keyof typeof Status; // enum key
color?: typeof Colors[keyof typeof Colors]; // object value
}
const isTask = StrictTypeGuardBuilder
.start<Task>('Task')
.validateProperty('title', CommonTypeGuards.basics.string())
.validateProperty('status', CommonTypeGuards.enums.memberOf(Status)) // validates 'active', 'inactive', 'pending'
.validateProperty('priority', CommonTypeGuards.enums.memberOf(Priority)) // validates 1, 2, 3
.validateProperty('statusKey', CommonTypeGuards.enums.keyOf(Status).nullable(undefined)) // validates 'Active', 'Inactive', 'Pending'
.validateProperty('color', CommonTypeGuards.enums.memberOf(Colors).nullable(undefined)) // validates 'red', 'green', 'blue'
.build();
// Example usage demonstrating the difference
const statusValue = 'active'; // This is a Status enum value
const statusKey = 'Active'; // This is a Status enum key
if (CommonTypeGuards.enums.memberOf(Status)(statusValue)) {
// statusValue is now typed as Status ('active' | 'inactive' | 'pending')
}
if (CommonTypeGuards.enums.keyOf(Status)(statusKey)) {
// statusKey is now typed as keyof typeof Status ('Active' | 'Inactive' | 'Pending')
}Advanced Usage
Custom Type Guards
Create your own type guard predicates for complex validation:
interface User {
id: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
// Custom email validator
const isEmail = (obj: unknown): obj is string => {
return typeof obj === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(obj);
};
// Custom role validator
const isValidRole = (obj: unknown): obj is 'admin' | 'user' | 'guest' => {
return obj === 'admin' || obj === 'user' || obj === 'guest';
};
const isUser = StrictTypeGuardBuilder
.start<User>('User')
.validateProperty('id', CommonTypeGuards.basics.string())
.validateProperty('email', isEmail)
.validateProperty('role', isValidRole)
.build();Nested Object Validation
interface Address {
street: string;
city: string;
zipCode: string;
}
interface Person {
name: string;
address: Address;
secondaryAddress?: Address;
}
// First, create a type guard for the nested object
const isAddress = StrictTypeGuardBuilder
.start<Address>('Address')
.validateProperty('street', CommonTypeGuards.basics.string())
.validateProperty('city', CommonTypeGuards.basics.string())
.validateProperty('zipCode', CommonTypeGuards.basics.string())
.build();
// Then use it in the parent object
const isPerson = StrictTypeGuardBuilder
.start<Person>('Person')
.validateProperty('name', CommonTypeGuards.basics.string())
.validateProperty('address', isAddress)
.validateProperty('secondaryAddress', isAddress.nullable(undefined))
.build();Root-Level Validation
Sometimes you need to validate the entire object structure or want to bypass individual property validation. The validateRoot() method allows you to validate the complete object and automatically satisfies the StrictTypeGuardBuilder's requirement to validate all properties:
interface Coordinates {
x: number;
y: number;
}
// Custom validator for the entire object
const isValidCoordinates = (obj: unknown): obj is Coordinates => {
if (typeof obj !== 'object' || !obj) return false;
const coords = obj as Coordinates;
return typeof coords.x === 'number'
&& typeof coords.y === 'number'
&& coords.x >= 0 && coords.x <= 100
&& coords.y >= 0 && coords.y <= 100;
};
// Root validation allows immediate build() - no individual properties needed
const isCoordinates = StrictTypeGuardBuilder
.start<Coordinates>('Coordinates')
.validateRoot(isValidCoordinates)
.build(); // ✅ Compiles immediately
// You can also combine root validation with property validation
const isCoordinatesWithDetails = StrictTypeGuardBuilder
.start<Coordinates>('Coordinates')
.validateProperty('x', CommonTypeGuards.basics.number())
.validateProperty('y', CommonTypeGuards.basics.number())
.validateRoot(isValidCoordinates) // Additional validation on top
.build();Key Point: validateRoot() tells the StrictTypeGuardBuilder that you're handling the entire object validation yourself, so it won't require individual property validations.
Nullable Objects
Build type guards that accept null or undefined:
const isUserOrNull = StrictTypeGuardBuilder
.start<User>('User')
.validateProperty('id', CommonTypeGuards.basics.string())
.validateProperty('name', CommonTypeGuards.basics.string())
.validateProperty('email', CommonTypeGuards.basics.string())
.build().nullable(); // Returns (obj: unknown) => obj is User | null | undefined
// Or be more specific about which nullish values to allow
const isUserOrJustNull = StrictTypeGuardBuilder
.start<User>('User')
.validateProperty('id', CommonTypeGuards.basics.string())
.validateProperty('name', CommonTypeGuards.basics.string())
.validateProperty('email', CommonTypeGuards.basics.string())
.build().nullable(null); // Returns (obj: unknown) => obj is User | nullTypeGuardBuilder vs StrictTypeGuardBuilder
This library provides two builders:
StrictTypeGuardBuilder (Recommended)
- Compile-time validation: Ensures all properties are validated or ignored
- Type safety: Prevents you from forgetting to validate properties
- Better developer experience: Clear error messages guide you to missing validations
- Strict type matching: Requires type guards to exactly match the property type (e.g.,
string | nullrequiresstring | null, not juststring)
Important: When using StrictTypeGuardBuilder, you must validate or ignore all properties before calling build() or build().nullable(). If you don't, TypeScript will show a compile error indicating which properties are missing validation.
Exception: If you call validateRoot(), it assumes the entire object structure will be validated at the root level, allowing you to call build() or build().nullable() immediately without validating individual properties.
TypeGuardBuilder
- More flexible: Allows partial property validation
- Runtime warnings: Shows warnings for unvalidated properties
- Backward compatibility: Easier migration from manual type guards
// StrictTypeGuardBuilder - compile error if you miss a property
const strictGuard = StrictTypeGuardBuilder
.start<User>('User')
.validateProperty('id', CommonTypeGuards.basics.string())
// Missing 'name' and 'email' - TypeScript error!
.build(); // ❌ Compile error: "Missing required properties"
// With root validation - bypasses individual property requirements
const rootValidatedGuard = StrictTypeGuardBuilder
.start<User>('User')
.validateRoot(myCustomUserValidator) // Validates entire object
.build(); // ✅ Compiles immediately
// TypeGuardBuilder - allows partial validation
const flexibleGuard = TypeGuardBuilder
.start<User>('User')
.validateProperty('id', CommonTypeGuards.basics.string())
// Missing properties will show runtime warnings
.build(); // ✅ Compiles, but shows warnings at runtimeUnderstanding StrictTypeGuardBuilder Compilation Errors
When you forget to validate properties, TypeScript will show helpful error messages:
interface User {
id: string;
name: string;
email: string;
age: number;
}
const incompleteGuard = StrictTypeGuardBuilder
.start<User>('User')
.validateProperty('id', CommonTypeGuards.basics.string())
.validateProperty('name', CommonTypeGuards.basics.string())
// Missing 'email' and 'age'
.build(); // ❌ Error: Missing required properties: "email" | "age"The error message tells you exactly which properties need validation, making it easy to fix.
Troubleshooting Type Mismatch Errors
When your type guard doesn't match the expected property type, you'll see descriptive TypeScript errors that guide you to the correct fix.
Nullability Mismatch Errors
For nullability mismatches (when the nullable/undefined handling doesn't match), the errors follow this format:
TS2345: Argument of type TypeGuardPredicateWithNullable<number> is not assignable to parameter of type
"Incompatibile nullability: Property expects 'null' but type guard doesn't handle it. Add .nullable(null)"The error message tells you exactly what's wrong and how to fix it:
"Property expects 'null' but type guard doesn't handle it. Add .nullable(null)"- Your property isT | nullbut you're using a non-nullable type guard"Property expects 'undefined' but type guard doesn't handle it. Add .nullable(undefined)"- Your property isT | undefinedbut you're using a non-nullable type guard"Property expects 'null | undefined' but type guard doesn't handle nullability. Add .nullable()"- Your property isT | null | undefinedbut you're using a non-nullable type guard"Property is not nullable but type guard handles nullability. Remove .nullable()"- Your property is justTbut you're using.nullable()
These nullability validation errors ensure strict type matching between your interface properties and type guards, preventing runtime type mismatches involving null and undefined values.
Debugging and Logging
The library provides helpful debugging features:
Console Warnings
When validation fails, you'll see detailed console warnings:
// If validation fails, you'll see:
// "Validation failed for property 'email' in 'User'. Value received: 'not-an-email'"
// "No validator specified for property 'unexpected' in 'User'"Controlling Log Output
You can control whether actual values are logged (useful for sensitive data):
import { TypeGuardBuilder } from '@bpits/type-guards';
// Disable logging of actual values for security
TypeGuardBuilder.LogValueReceived = false;
// Now failed validations will show "redacted" instead of actual valuesYou can also suppress validation warnings entirely (useful for production environments or testing):
import { TypeGuardBuilder } from '@bpits/type-guards';
// Disable all validation warnings
TypeGuardBuilder.LogValidationWarnings = false;
// Now validation failures will no longer produce console warningsReal-World Examples
API Response Validation
interface ApiUser {
id: string;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
avatar?: string;
};
roles: string[];
lastLogin: string; // ISO date string
isActive: boolean;
}
const isProfile = StrictTypeGuardBuilder
.start<ApiUser['profile']>('UserProfile')
.validateProperty('firstName', CommonTypeGuards.basics.string())
.validateProperty('lastName', CommonTypeGuards.basics.string())
.validateProperty('avatar', CommonTypeGuards.basics.string().nullable(undefined))
.build();
const isApiUser = StrictTypeGuardBuilder
.start<ApiUser>('ApiUser')
.validateProperty('id', CommonTypeGuards.basics.string())
.validateProperty('username', CommonTypeGuards.basics.string())
.validateProperty('email', CommonTypeGuards.basics.string())
.validateProperty('profile', isProfile)
.validateProperty('roles', CommonTypeGuards.array.arrayOf(CommonTypeGuards.basics.string()))
.validateProperty('lastLogin', CommonTypeGuards.date.dateString())
.validateProperty('isActive', CommonTypeGuards.basics.boolean())
.build();
// Usage in API call
async function fetchUser(id: string): Promise<ApiUser | null> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
if (isApiUser(data)) {
return data; // Fully typed as ApiUser
}
console.error('Invalid user data received from API');
return null;
}Best Practices
1. Use Descriptive Type Names
// Good
StrictTypeGuardBuilder.start<User>('User')
StrictTypeGuardBuilder.start<ApiResponse>('WeatherApiResponse')
// Less helpful
StrictTypeGuardBuilder.start<User>('obj')2. Create Reusable Validators
// Create common validators for your domain
const isPositiveNumber = (obj: unknown): obj is number => {
return typeof obj === 'number' && obj > 0;
};
const isValidEmail = (obj: unknown): obj is string => {
return typeof obj === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(obj);
};3. Validate at Boundaries
// Validate data when it enters your system
async function fetchUsers(): Promise<User[]> {
const response = await fetch('/api/users');
const data = await response.json();
if (CommonTypeGuards.array.arrayOf(isUser)(data)) {
return data; // Now safely typed
}
throw new Error('Invalid user data from API');
}4. Combine with Error Handling
function processApiData(data: unknown): User | null {
try {
if (isUser(data)) {
return data;
}
console.warn('Data validation failed, using defaults');
return null;
} catch (error) {
console.error('Error processing API data:', error);
return null;
}
}TypeScript Configuration
For the best experience, ensure your tsconfig.json includes:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}Contributing
This library is designed to be extensible. You can contribute by:
- Adding new common type guards
- Improving error messages
- Adding utility functions
- Improving documentation
License
MIT License - see LICENSE file for details.
