Function argument validation for humans with expressive, chainable API
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Optional predicates, custom validation, error handling, TypeScript type utilities, and advanced validation patterns for complex use cases.
Make any predicate optional by allowing undefined values alongside the main type.
/** Optional predicates interface */
interface OptionalPredicates {
// Primitive types
readonly string: StringPredicate & BasePredicate<string | undefined>;
readonly number: NumberPredicate & BasePredicate<number | undefined>;
readonly boolean: BooleanPredicate & BasePredicate<boolean | undefined>;
readonly bigint: BigIntPredicate & BasePredicate<bigint | undefined>;
readonly symbol: Predicate<symbol | undefined>;
// Special values
readonly undefined: Predicate<undefined>; // Always passes
readonly null: Predicate<null | undefined>;
readonly nullOrUndefined: Predicate<null | undefined>; // Always passes
readonly nan: Predicate<number | undefined>;
// Built-in types
readonly array: ArrayPredicate & BasePredicate<unknown[] | undefined>;
readonly object: ObjectPredicate & BasePredicate<object | undefined>;
readonly date: DatePredicate & BasePredicate<Date | undefined>;
readonly error: ErrorPredicate & BasePredicate<Error | undefined>;
readonly regExp: Predicate<RegExp | undefined>;
readonly promise: Predicate<Promise<unknown> | undefined>;
readonly function: Predicate<Function | undefined>;
readonly buffer: Predicate<Buffer | undefined>;
// Collection types
readonly map: MapPredicate & BasePredicate<Map<unknown, unknown> | undefined>;
readonly set: SetPredicate & BasePredicate<Set<unknown> | undefined>;
readonly weakMap: WeakMapPredicate & BasePredicate<WeakMap<object, unknown> | undefined>;
readonly weakSet: WeakSetPredicate & BasePredicate<WeakSet<object> | undefined>;
readonly iterable: Predicate<Iterable<unknown> | undefined>;
// Typed arrays
readonly typedArray: TypedArrayPredicate<TypedArray | undefined>;
readonly int8Array: TypedArrayPredicate<Int8Array | undefined>;
readonly uint8Array: TypedArrayPredicate<Uint8Array | undefined>;
readonly uint8ClampedArray: TypedArrayPredicate<Uint8ClampedArray | undefined>;
readonly int16Array: TypedArrayPredicate<Int16Array | undefined>;
readonly uint16Array: TypedArrayPredicate<Uint16Array | undefined>;
readonly int32Array: TypedArrayPredicate<Int32Array | undefined>;
readonly uint32Array: TypedArrayPredicate<Uint32Array | undefined>;
readonly float32Array: TypedArrayPredicate<Float32Array | undefined>;
readonly float64Array: TypedArrayPredicate<Float64Array | undefined>;
readonly arrayBuffer: ArrayBufferPredicate<ArrayBuffer | undefined>;
readonly sharedArrayBuffer: ArrayBufferPredicate<SharedArrayBuffer | undefined>;
readonly dataView: DataViewPredicate & BasePredicate<DataView | undefined>;
}
/** Access to optional predicates */
const optional: OptionalPredicates;Usage Examples:
import ow from 'ow';
// Optional primitive types
ow('hello', ow.optional.string);
ow(undefined, ow.optional.string);
ow(42, ow.optional.number.positive);
ow(undefined, ow.optional.number.positive);
// Optional objects and arrays
ow({ name: 'Alice' }, ow.optional.object.hasKeys('name'));
ow(undefined, ow.optional.object.hasKeys('name'));
ow([1, 2, 3], ow.optional.array.length(3));
ow(undefined, ow.optional.array.length(3));
// Function parameters with optional validation
function processUser(name: unknown, age?: unknown, email?: unknown) {
ow(name, 'name', ow.string.nonEmpty);
ow(age, 'age', ow.optional.number.integer.positive);
ow(email, 'email', ow.optional.string.matches(/^.+@.+\..+$/));
return {
name: name as string,
age: age as number | undefined,
email: email as string | undefined
};
}
// Usage
processUser('Alice', 30, 'alice@example.com');
processUser('Bob', undefined, undefined);
processUser('Charlie', 25); // age provided, email undefinedAdvanced custom validation with flexible validation functions and error messages.
/** Custom validation interfaces */
interface CustomValidator<T> {
validator: boolean;
message: string | MessageBuilder;
}
type MessageBuilder = (label?: string) => string;
type ValidationFunction<T> = (value: T) => boolean | string;
interface BasePredicate<T> {
/** Custom validation function returning boolean or error string */
is(validator: ValidationFunction<T>): BasePredicate<T>;
/** Custom validation with validator/message object */
validate(customValidator: CustomValidator<T>): BasePredicate<T>;
/** Override default error message */
message(newMessage: string | MessageBuilder<T>): BasePredicate<T>;
}
type MessageBuilder<T> = (value: T, label?: string) => string;Custom Function Examples:
import ow from 'ow';
// Simple boolean validation
ow(8, ow.number.is(x => x % 2 === 0));
// Custom validation with error message
ow(7, ow.number.is(x => x % 2 === 0 || `Expected even number, got ${x}`));
// Complex string validation
ow('password123', ow.string.is(s => {
if (s.length < 8) return 'Password must be at least 8 characters';
if (!/\d/.test(s)) return 'Password must contain at least one digit';
if (!/[a-z]/.test(s)) return 'Password must contain at least one lowercase letter';
return true;
}));
// Array validation
ow([1, 2, 3, 4], ow.array.is(arr =>
arr.every(x => typeof x === 'number') || 'All elements must be numbers'
));Custom Validator Object Examples:
import ow from 'ow';
// Validator object with static message
ow(15, ow.number.validate(value => ({
validator: value > 10,
message: `Expected number greater than 10, got ${value}`
})));
// Validator object with dynamic message
ow(5, 'threshold', ow.number.validate(value => ({
validator: value > 10,
message: label => `Expected ${label} to be greater than 10, got ${value}`
})));
// Complex object validation
const userValidator = (user: any) => ({
validator: user.age >= 18 && user.email.includes('@'),
message: 'User must be 18+ with valid email'
});
ow({ name: 'Alice', age: 25, email: 'alice@example.com' },
ow.object.validate(userValidator));Custom Message Examples:
import ow from 'ow';
// Static custom message
ow('hi', 'greeting', ow.string.minLength(5).message('Greeting is too short'));
// Dynamic custom message
ow(3, ow.number.positive.message((value, label) =>
`Expected positive ${label || 'number'}, got ${value}`
));
// Chained custom messages
ow('1234', ow.string
.minLength(5).message('Password too short')
.matches(/\d/).message('Password missing numbers')
);Advanced TypeScript integration with type inference and assertion utilities.
/** Extract TypeScript type from predicate */
type Infer<P> = P extends BasePredicate<infer T> ? T : never;
/** Reusable validator function */
type ReusableValidator<T> = (value: unknown, label?: string) => void;
/** Type assertion version of reusable validator */
type AssertingValidator<T> = (value: unknown, label?: string) => asserts value is T;
/** Convert ReusableValidator to AssertingValidator */
function asAssertingValidator<T>(
validator: ReusableValidator<T>
): AssertingValidator<T>;Type Inference Examples:
import ow, { Infer } from 'ow';
// Extract types from predicates
const userPredicate = ow.object.exactShape({
id: ow.number.integer.positive,
name: ow.string.nonEmpty,
email: ow.optional.string.matches(/^.+@.+\..+$/),
roles: ow.array.ofType(ow.string.oneOf(['admin', 'user', 'guest']))
});
type User = Infer<typeof userPredicate>;
// Result: {
// id: number;
// name: string;
// email?: string | undefined;
// roles: string[];
// }
// Use inferred type
function processUser(userData: unknown): User {
ow(userData, userPredicate);
return userData as User; // TypeScript knows this is safe
}
// Complex nested type inference
const apiResponsePredicate = ow.object.exactShape({
data: ow.array.ofType(userPredicate),
meta: ow.object.exactShape({
total: ow.number.integer.positive,
page: ow.number.integer.positive,
hasMore: ow.boolean
})
});
type ApiResponse = Infer<typeof apiResponsePredicate>;Reusable Validator Examples:
import ow from 'ow';
// Create reusable validators
const validateEmail = ow.create('email', ow.string.matches(/^.+@.+\..+$/));
const validateUserId = ow.create(ow.number.integer.positive);
// Use reusable validators
validateEmail('alice@example.com');
validateUserId(123);
// Create validator with specific label
const validatePassword = ow.create('password',
ow.string.minLength(8).is(s => /\d/.test(s) || 'Must contain digit')
);
validatePassword('secret123');
// Type assertion validators
function createUser(email: unknown, id: unknown) {
validateEmail(email);
validateUserId(id);
// TypeScript knows email is string and id is number
return {
id: id as number,
email: email as string,
createdAt: new Date()
};
}Advanced error handling with ArgumentError and validation patterns.
/** Error thrown when validation fails */
class ArgumentError extends Error {
readonly name: 'ArgumentError';
constructor(message: string);
}
/** Boolean validation that doesn't throw */
function isValid<T>(value: unknown, predicate: BasePredicate<T>): value is T;Error Handling Examples:
import ow, { ArgumentError } from 'ow';
// Basic error handling
try {
ow('invalid-email', ow.string.matches(/^.+@.+\..+$/));
} catch (error) {
if (error instanceof ArgumentError) {
console.log('Validation failed:', error.message);
}
}
// Non-throwing validation
function safeValidate(data: unknown) {
if (ow.isValid(data, ow.object.hasKeys('id', 'name'))) {
// TypeScript knows data is object with id and name properties
return { success: true, data };
}
return { success: false, error: 'Invalid data structure' };
}
// Validation with error context
function validateApiRequest(request: unknown) {
try {
ow(request, 'request', ow.object.exactShape({
method: ow.string.oneOf(['GET', 'POST', 'PUT', 'DELETE']),
url: ow.string.url,
headers: ow.optional.object,
body: ow.optional.any(ow.object, ow.string)
}));
return { valid: true, request };
} catch (error) {
if (error instanceof ArgumentError) {
return {
valid: false,
error: error.message,
type: 'validation_error'
};
}
throw error; // Re-throw unexpected errors
}
}Complex validation patterns for real-world scenarios.
import ow from 'ow';
// Conditional validation
function validateUser(userData: unknown, isAdmin = false) {
const baseShape = {
id: ow.number.integer.positive,
name: ow.string.nonEmpty,
email: ow.string.matches(/^.+@.+\..+$/)
};
const adminShape = {
...baseShape,
permissions: ow.array.ofType(ow.string.nonEmpty),
lastLogin: ow.optional.date
};
ow(userData, isAdmin ? ow.object.exactShape(adminShape) : ow.object.exactShape(baseShape));
}
// Multi-step validation
function processFormData(formData: unknown) {
// Step 1: Validate basic structure
ow(formData, ow.object);
// Step 2: Validate required fields
ow(formData, ow.object.hasKeys('name', 'email'));
// Step 3: Validate field types and constraints
const data = formData as Record<string, unknown>;
ow(data.name, 'name', ow.string.nonEmpty.maxLength(100));
ow(data.email, 'email', ow.string.matches(/^.+@.+\..+$/));
// Step 4: Validate optional fields if present
if ('age' in data) {
ow(data.age, 'age', ow.number.integer.inRange(0, 120));
}
if ('website' in data) {
ow(data.website, 'website', ow.string.url);
}
return data;
}
// Polymorphic validation
function validateShape(shape: unknown) {
// First determine the shape type
ow(shape, ow.object.hasKeys('type'));
const typedShape = shape as { type: string };
switch (typedShape.type) {
case 'circle':
ow(shape, ow.object.exactShape({
type: ow.string.equals('circle'),
radius: ow.number.positive
}));
break;
case 'rectangle':
ow(shape, ow.object.exactShape({
type: ow.string.equals('rectangle'),
width: ow.number.positive,
height: ow.number.positive
}));
break;
default:
throw new Error(`Unknown shape type: ${typedShape.type}`);
}
}
// Recursive validation
const treeNodePredicate = ow.object.exactShape({
id: ow.string.nonEmpty,
value: ow.any(ow.string, ow.number),
children: ow.optional.array.ofType(ow.object.hasKeys('id', 'value'))
});
function validateTree(node: unknown) {
ow(node, treeNodePredicate);
const typedNode = node as any;
if (typedNode.children) {
typedNode.children.forEach((child: unknown, index: number) => {
validateTree(child); // Recursive validation
});
}
}Install with Tessl CLI
npx tessl i tessl/npm-ow