Skip to content

tuulbelt/property-validator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Property Validator / propval

Tests Version Node Dogfooded Tests Zero Dependencies Performance License

Runtime type validation with TypeScript inference. Great developer experience and high performance, with zero external dependencies.

Problem

TypeScript provides excellent compile-time type safety, but those types disappear at runtime. When data crosses boundaries (API responses, user input, file parsing), you need runtime validation that stays in sync with your TypeScript types.

Property Validator provides:

  • Intuitive API — Fluent builder pattern that feels natural to write
  • High Performance — JIT compilation for validation-heavy workloads
  • Full Type Inference — TypeScript knows your validated types
  • Zero Dependencies — Uses only Node.js standard library
  • Three API Tiers — Choose your speed vs. detail trade-off

Features

  • Zero runtime dependencies (Node.js standard library only)
  • TypeScript-first design with automatic type inference
  • Framework-agnostic (works with React, Vue, Svelte, vanilla JS)
  • Clear, actionable error messages
  • Composable validators for complex types
  • Works as CLI tool or library

Installation

Clone the repository:

git clone https://github.com/tuulbelt/property-validator.git
cd property-validator
npm install  # Install dev dependencies only

No runtime dependencies — this tool uses only Node.js standard library.

CLI names — both short and long forms work:

  • Short (recommended): propval
  • Long: property-validator

Recommended setup — install globally for easy access:

npm link  # Enable the 'propval' command globally
propval --help

For local development without global install:

npx tsx src/index.ts --help

Usage

As a Library

Fluent API (v namespace):

import { v, validate } from '@tuulbelt/property-validator';

// Define validators with built-in constraints
const userValidator = v.object({
  name: v.string().min(1).max(100),
  age: v.number().int().positive().max(150),
  email: v.string().email()
});

// Validate data
const result = validate(userValidator, {
  name: "Alice",
  age: 30,
  email: "alice@example.com"
});

if (result.ok) {
  console.log(result.value); // Typed as { name: string, age: number, email: string }
} else {
  console.error(result.error); // Clear error message
}

Named imports (tree-shakeable):

// Import named validators from main entry point
import { validate, string, number, object, email, int, positive } from '@tuulbelt/property-validator';

const userValidator = object({
  name: string(),
  age: number(int(), positive()),
  email: string(email())
});

const result = validate(userValidator, data);

As a CLI

Using short name (recommended after npm link):

# Validate JSON from stdin
echo '{"name":"Alice","age":30}' | propval --schema user.schema.json

# Validate a file
propval --schema user.schema.json data.json

# Show help
propval --help

Using long name:

property-validator --schema user.schema.json data.json

Modular Imports (v0.9.0+)

Property Validator supports tree-shaking for smaller bundle sizes. Import only what you need:

// Named exports (tree-shakeable)
import { string, number, object, validate } from '@tuulbelt/property-validator';

const userValidator = object({
  name: string().min(1),
  age: number().positive()
});

const result = validate(userValidator, data);

Available named exports (main entry):

  • Validators: string, number, boolean, array, tuple, object, record, optional, nullable, union, discriminatedUnion, literal, lazy, enum_
  • Functions: validate, check, compile, compileCheck, toJsonSchema
  • Class: ValidationError

Fluent API (v namespace from main entry):

// v namespace is available from the main entry point
import { v, validate } from '@tuulbelt/property-validator';

const schema = v.object({ name: v.string() });

Type-only imports:

import type { Validator, Result, ValidationOptions } from '@tuulbelt/property-validator/types';

Entry Points (v0.9.2+)

Property Validator provides multiple entry points for different use cases:

Entry Point Import From Use Case
Main @tuulbelt/property-validator Full API (v namespace + named exports)
/types @tuulbelt/property-validator/types Type definitions only

Example: Main entry point (all APIs):

import { v, validate, check, string, number, object } from '@tuulbelt/property-validator';

// Fluent API with v namespace
const UserSchema = v.object({
  name: v.string().email(),
  age: v.number().positive()
});

// Or functional API with named exports
const AgeSchema = number(int(), positive());

const result = validate(UserSchema, data);

Both styles from one import:

  • Fluent API: v.string().email() — Compact, chainable syntax
  • Functional API: string(email()) — Explicit imports, tree-shakeable refinements

Functional Refinement API (v0.9.1+)

For maximum tree-shaking potential, use the functional refinement pattern inspired by Valibot:

import { string, number, email, minLength, int, positive, validate } from '@tuulbelt/property-validator';

// Refinements are separate function exports - bundlers can exclude unused ones
const EmailSchema = string(email(), minLength(5));
const AgeSchema = number(int(), positive());

validate(EmailSchema, 'test@example.com'); // ✓
validate(AgeSchema, 25); // ✓

Available refinement exports:

String refinements:

  • Length: minLength(n), maxLength(n), length(n), nonempty()
  • Format: email(), url(), uuid(), pattern(regex, message?)
  • Content: startsWith(prefix), endsWith(suffix), includes(substring)
  • Date/Time: datetime(), date(), time()
  • Network: ip(), ipv4(), ipv6()

Number refinements:

  • Type: int(), safeInt(), finite()
  • Sign: positive(), negative(), nonnegative(), nonpositive()
  • Range: min(n), max(n), range(min, max), multipleOf(n)

Array refinements:

  • Length: minItems(n), maxItems(n), itemCount(n), nonemptyArray()

Comparison: Chainable vs Functional API:

// Chainable API (compact, all methods bundled)
const schema1 = v.string().email().min(5);

// Functional API (tree-shakeable, explicit imports)
import { string, email, minLength } from '@tuulbelt/property-validator';
const schema2 = string(email(), minLength(5));

// Both produce equivalent validators!

When to use which:

  • Chainable API (v.string().email()): Compact syntax, great for quick prototyping
  • Functional API (string(email())): Maximum tree-shaking, explicit about what's used

Bundle Size & Design Philosophy

Import Style Size (minified) Size (gzipped)
Full bundle 30 KB 8 KB

Why 30KB? — Performance-First Architecture

Property Validator prioritizes validation speed over bundle size. The bundle includes:

  • JIT Compilation Engine — Validators compile to optimized functions at schema definition time, not at first validation. This means zero startup cost when validating.

  • Pre-computed Fast Pathscheck() and compileCheck() bypass error handling entirely, achieving sub-100ns validation for most schemas.

  • Unified Validation Machinery — All validator types share optimized internals. This enables consistent ~60ns validation across primitives, objects, arrays, and unions.

The Trade-off:

Approach Bundle Size Validation Speed
Minimal validators (lazy compilation) ~15 KB 200-500 ns (first call compiles)
Property Validator 30 KB 55-170 ns (pre-compiled)

We chose speed. For applications validating API responses, form inputs, or processing data pipelines, the 15KB difference (~2KB gzipped) is negligible compared to React (~40KB), but the 3-5x speed improvement compounds across every validation call.

Tree-Shaking Benefits:

Named exports provide organizational clarity and some bundle reduction:

  1. Refinements are tree-shakeable — Only imported refinements like email(), uuid(), minLength() are bundled
  2. Type definitions excluded — Import from /types for zero runtime cost
  3. Code organization — Explicit imports make dependencies clear

API

Core Functions

validate<T>(validator: Validator<T>, data: unknown): Result<T>

Full validation with detailed error messages.

Parameters:

  • validator — Validator instance (created with v.* functions)
  • data — Unknown data to validate

Returns:

  • Result<T> object with:
    • ok: true and value: T if validation succeeded
    • ok: false and error: ValidationError if validation failed

Best for: Form validation, API responses, anywhere you need error details.

const result = validate(UserSchema, data);
if (!result.ok) {
  console.log(result.error.format('text'));  // Human-readable error
}

check<T>(validator: Validator<T>, data: unknown): boolean

Fast boolean-only validation. Skips error path computation entirely.

Parameters:

  • validator — Validator instance (created with v.* functions)
  • data — Unknown data to validate

Returns:

  • true if valid, false if invalid

Best for: Conditionals, filtering, type guards, anywhere you only need pass/fail.

import { v, check } from '@tuulbelt/property-validator';

const UserSchema = v.object({ name: v.string(), age: v.number() });

// Use in conditionals
if (check(UserSchema, data)) {
  processUser(data);  // data is valid
}

// Use for filtering
const validUsers = users.filter(u => check(UserSchema, u));

compileCheck<T>(validator: Validator<T>): (data: unknown) => boolean

Pre-compile a validator for maximum-speed boolean validation. Returns a cached function.

Parameters:

  • validator — Validator instance to compile

Returns:

  • (data: unknown) => boolean — Compiled check function

Best for: Hot paths, large datasets, performance-critical loops.

import { v, compileCheck } from '@tuulbelt/property-validator';

const UserSchema = v.object({ name: v.string(), age: v.number() });
const isValidUser = compileCheck(UserSchema);  // Compile once

// Use in hot loops (maximum speed)
for (const user of users) {
  if (isValidUser(user)) {
    processUser(user);
  }
}

Choosing the Right API

Use Case API Why
Form validation validate() Need error messages for UX
API request validation validate() Need detailed errors for debugging
Type guards / conditionals check() Simple pass/fail, faster
Filtering arrays check() Boolean predicate needed
High-throughput pipelines compileCheck() Maximum speed, pre-compiled
Validating same schema 1000+ times compileCheck() Compilation overhead amortized

Validator Builders

Primitives:

  • v.string() — String validator
  • v.number() — Number validator
  • v.boolean() — Boolean validator

Built-in String Constraints:

  • .min(n) / .max(n) / .length(n) — Length constraints
  • .nonempty() — Requires non-empty string
  • .email() — Valid email address
  • .url() — Valid HTTP/HTTPS URL
  • .uuid() — Valid UUID (v1-v5)
  • .pattern(regex, message?) — Custom regex pattern
  • .startsWith(prefix) / .endsWith(suffix) / .includes(substring)

Built-in Number Constraints:

  • .int() — Integer only
  • .positive() / .negative() — Sign constraints (exclusive)
  • .nonnegative() / .nonpositive() — Sign constraints (inclusive)
  • .min(n) / .max(n) — Value bounds
  • .range(min, max) — Value range (inclusive)
  • .finite() — Not Infinity or NaN
  • .safeInt() — Safe integer range

Collections:

  • v.array(itemValidator) — Array validator (homogeneous elements)
    • .min(n) — Minimum length constraint
    • .max(n) — Maximum length constraint
    • .length(n) — Exact length constraint
    • .nonempty() — Requires at least 1 element
  • v.tuple([...validators]) — Tuple validator (fixed-length, per-index types)

Objects:

  • v.object(shape) — Object validator with shape
    • .strict() — Reject objects with unknown properties (v0.10.0+)
    • .passthrough() — Allow unknown properties in output (v0.10.0+)
  • v.record(keyValidator, valueValidator) — Dynamic key-value pairs (v0.10.0+)

Unions and Literals:

  • v.union([validator1, validator2, ...]) — Union validator (OR logic, validates if any schema matches)
  • v.discriminatedUnion(discriminator, variants) — Efficient tagged unions (v0.10.0+)
  • v.literal(value) — Literal validator (exact value matching using ===)
  • v.enum(['a', 'b', 'c']) — Enum validator (union of string literals)

Modifiers:

  • v.optional(validator) — Optional field (allows undefined) [deprecated: use .optional() method]
  • v.nullable(validator) — Nullable field (allows null) [deprecated: use .nullable() method]

Chainable Methods (all validators):

  • .refine(predicate, message) — Add custom validation logic
  • .transform(fn) — Transform validated value (changes type)
  • .optional() — Allow undefined
  • .nullable() — Allow null
  • .nullish() — Allow undefined or null
  • .default(value) — Provide default value (static or lazy function)

Array Examples

// Basic array validation
const numbersValidator = v.array(v.number());
validate(numbersValidator, [1, 2, 3]); // ✓

// Array with length constraints
const tagsValidator = v.array(v.string()).min(1).max(5);
validate(tagsValidator, ['typescript', 'validation']); // ✓

// Array of objects
const usersValidator = v.array(v.object({
  name: v.string(),
  age: v.number()
}));
validate(usersValidator, [
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 25 }
]); // ✓

// Nested arrays (2D matrix)
const matrixValidator = v.array(v.array(v.number()));
validate(matrixValidator, [[1, 2], [3, 4]]); // ✓

Tuple Examples

// Coordinate tuple [x, y]
const coordValidator = v.tuple([v.number(), v.number()]);
validate(coordValidator, [10, 20]); // ✓

// Mixed-type tuple
const personValidator = v.tuple([
  v.string(),   // name
  v.number(),   // age
  v.boolean()   // active
]);
validate(personValidator, ['Alice', 30, true]); // ✓

// Tuple with optional field
const entryValidator = v.tuple([
  v.string(),
  v.optional(v.number())
]);
validate(entryValidator, ['key', undefined]); // ✓
validate(entryValidator, ['key', 42]); // ✓

Union Examples

// Simple union (string | number)
const stringOrNumber = v.union([v.string(), v.number()]);
validate(stringOrNumber, 'hello'); // ✓
validate(stringOrNumber, 42); // ✓
validate(stringOrNumber, true); // ✗

// Discriminated unions (tagged unions)
const apiResponse = v.union([
  v.object({ type: v.literal('success'), data: v.string() }),
  v.object({ type: v.literal('error'), message: v.string() })
]);
validate(apiResponse, { type: 'success', data: 'OK' }); // ✓
validate(apiResponse, { type: 'error', message: 'Failed' }); // ✓

// Enum as union sugar
const statusValidator = v.enum(['active', 'inactive', 'pending']);
validate(statusValidator, 'active'); // ✓
validate(statusValidator, 'archived'); // ✗

Discriminated Union Examples (v0.10.0+)

// Efficient tagged unions with discriminator field
const apiResponse = v.discriminatedUnion('type', {
  success: v.object({ type: v.literal('success'), data: v.string() }),
  error: v.object({ type: v.literal('error'), code: v.number(), message: v.string() }),
  pending: v.object({ type: v.literal('pending'), retryAfter: v.number() })
});

// O(1) lookup by discriminator value (faster than v.union() for many variants)
validate(apiResponse, { type: 'success', data: 'OK' }); // ✓
validate(apiResponse, { type: 'error', code: 404, message: 'Not found' }); // ✓
validate(apiResponse, { type: 'unknown' }); // ✗ "Unknown discriminator value: unknown"

Record Examples (v0.10.0+)

// Dynamic key-value pairs
const scores = v.record(v.string(), v.number());
validate(scores, { alice: 100, bob: 85 }); // ✓
validate(scores, { alice: 'A' }); // ✗

// Record with key constraints
const uuidMap = v.record(v.string().uuid(), v.object({ name: v.string() }));
validate(uuidMap, { '550e8400-e29b-41d4-a716-446655440000': { name: 'Item' } }); // ✓

Strict and Passthrough Examples (v0.10.0+)

// Default: unknown properties are silently ignored
const user = v.object({ name: v.string() });
validate(user, { name: 'Alice', extra: true }); // ✓ (extra ignored)

// Strict: reject unknown properties
const strictUser = v.object({ name: v.string() }).strict();
validate(strictUser, { name: 'Alice' }); // ✓
validate(strictUser, { name: 'Alice', extra: true }); // ✗ "Unknown key: extra"

// Passthrough: preserve unknown properties in output
const passthroughUser = v.object({ name: v.string() }).passthrough();
const result = validate(passthroughUser, { name: 'Alice', extra: true });
// result.value = { name: 'Alice', extra: true } (extra preserved)

Built-in Validator Examples

// Email validation (built-in)
const email = v.string().email();
validate(email, 'alice@example.com'); // ✓
validate(email, 'not-an-email'); // ✗ "Must be a valid email address"

// URL validation (built-in)
const website = v.string().url();
validate(website, 'https://example.com'); // ✓

// Number constraints (built-in)
const age = v.number().int().positive().max(150);
validate(age, 25); // ✓
validate(age, -5); // ✗ "Number must be positive"
validate(age, 25.5); // ✗ "Number must be an integer"

// String constraints (built-in)
const username = v.string().min(3).max(20).pattern(/^[a-z0-9_]+$/);
validate(username, 'john_doe'); // ✓
validate(username, 'ab'); // ✗ "String must be at least 3 character(s)"

Refinement Examples (Custom Logic)

// Custom validation with .refine()
const password = v.string()
  .min(8)  // Built-in length check
  .refine(s => /[A-Z]/.test(s), 'Must contain uppercase letter')
  .refine(s => /[0-9]/.test(s), 'Must contain number');
validate(password, 'SecurePass123'); // ✓
validate(password, 'weak'); // ✗ "String must be at least 8 character(s)"

Transform Examples

// Parse string to integer
const parsedInt = v.string().transform(s => parseInt(s, 10));
const result = validate(parsedInt, '42');
if (result.ok) {
  console.log(result.value); // 42 (number)
}

// Trim and lowercase
const normalized = v.string()
  .transform(s => s.trim())
  .transform(s => s.toLowerCase());
validate(normalized, '  HELLO  '); // ✓ value: "hello"

// Transform with refinement
const positiveInt = v.string()
  .transform(s => parseInt(s, 10))
  .refine(n => n > 0, 'Must be positive integer');
validate(positiveInt, '42'); // ✓ value: 42
validate(positiveInt, '-5'); // ✗ "Must be positive integer"

Optional, Nullable, and Default Examples

// Optional field (allows undefined)
const optionalString = v.string().optional();
validate(optionalString, 'hello'); // ✓
validate(optionalString, undefined); // ✓
validate(optionalString, null); // ✗

// Nullable field (allows null)
const nullableNumber = v.number().nullable();
validate(nullableNumber, 42); // ✓
validate(nullableNumber, null); // ✓
validate(nullableNumber, undefined); // ✗

// Nullish (allows both undefined and null)
const nullishBoolean = v.boolean().nullish();
validate(nullishBoolean, true); // ✓
validate(nullishBoolean, undefined); // ✓
validate(nullishBoolean, null); // ✓

// Static default value
const withDefault = v.string().default('default-value');
validate(withDefault, 'custom'); // ✓ value: "custom"
validate(withDefault, undefined); // ✓ value: "default-value"

// Lazy default (function called each time)
const withTimestamp = v.number().default(() => Date.now());
validate(withTimestamp, undefined); // ✓ value: current timestamp

// Config with defaults
const configValidator = v.object({
  port: v.number().default(3000),
  host: v.string().default('localhost'),
  debug: v.boolean().default(false)
});
validate(configValidator, { port: undefined, host: undefined, debug: undefined });
// ✓ value: { port: 3000, host: "localhost", debug: false }

Custom Validators

import { v } from '@tuulbelt/property-validator';
import type { Validator } from '@tuulbelt/property-validator/types';

// Create custom validator
const emailValidator: Validator<string> = {
  validate(data: unknown): data is string {
    return typeof data === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data);
  },
  error(data: unknown): string {
    return `Expected email, got: ${typeof data}`;
  }
};

// Use in object schema
const userValidator = v.object({
  email: emailValidator
});

Examples

See the examples/ directory for runnable examples:

npx tsx examples/basic.ts

Testing

npm test              # Run all tests
npm test -- --watch   # Watch mode

Dogfooding

Tuulbelt tools validate each other via devDependencies. This tool uses:

  • test-flakiness-detector to validate test determinism
  • output-diffing-utility to verify validation output is deterministic

How It Works:

npm run dogfood           # Runs both flaky detection + output diff validation
npm run dogfood:flaky     # Runs tests 10 times to catch flaky tests
npm run dogfood:diff      # Validates output determinism via diff

This runs automatically in CI on every push/PR.

Why This Matters:

  • Validation must be deterministic (same input → same output, every time)
  • test-flakiness-detector ensures tests don't randomly fail
  • output-diffing-utility proves validation produces consistent results
  • Critical for caching, reproducible builds, and reliable testing

Configuration (package.json):

{
  "scripts": {
    "dogfood": "npm run dogfood:flaky && npm run dogfood:diff",
    "dogfood:flaky": "flaky --test 'npm test' --runs 10",
    "dogfood:diff": "bash scripts/dogfood-diff.sh"
  },
  "devDependencies": {
    "@tuulbelt/test-flakiness-detector": "git+https://github.com/tuulbelt/test-flakiness-detector.git",
    "@tuulbelt/output-diffing-utility": "git+https://github.com/tuulbelt/output-diffing-utility.git"
  }
}

See DOGFOODING_STRATEGY.md for the decision tree on when to add additional Tuulbelt tools as devDependencies.

Error Handling

Exit codes:

  • 0 — Success
  • 1 — Error (invalid input, validation failure)

Errors are returned in the error field of the result object, not thrown.

Performance

Property Validator is built for high-throughput validation with zero runtime dependencies. It offers three API tiers to match your performance needs.

API Performance Tiers

API Speed Returns Best For
validate() ~170 ns Result<T> with errors Forms, APIs, debugging
check() ~60 ns boolean Filtering, conditionals
compileCheck() ~55 ns boolean (pre-compiled) Hot paths, pipelines

Internal Comparison (v0.8.5)

Performance comparison across API tiers for common validation scenarios:

Scenario validate() check() compileCheck()
Simple Object 170 ns 58 ns 55 ns
Complex Nested 190 ns 60 ns 58 ns
Array (10 items) 250 ns 65 ns 63 ns
Array (100 items) 2.1 µs 145 ns 140 ns
Union (3 types) 90 ns 66 ns 56 ns

Key Insights:

  • check() is ~3x faster than validate() for valid data
  • compileCheck() adds another 5-15% on top of check()
  • The gap is largest for arrays and complex objects
  • For invalid data, check() is 6x+ faster (skips error path entirely)

How It Works

Property Validator uses JIT (Just-In-Time) compilation to optimize validation:

  1. Schema Definition — You define schemas with the fluent builder API
  2. Automatic JIT — On first validation, schemas compile to optimized functions
  3. Fast Pathcheck() and compileCheck() bypass error handling entirely
// Behind the scenes, this:
const UserSchema = v.object({ name: v.string(), age: v.number() });

// Compiles to something like:
const compiled = (data) =>
  typeof data === 'object' && data !== null &&
  typeof data.name === 'string' &&
  typeof data.age === 'number';

Detailed Benchmarks

For comprehensive benchmarks including comparisons with other libraries, see benchmarks/README.md.

Benchmarks use tatami-ng with criterion-equivalent statistical rigor (~1% variance).

JSON Schema Export (v0.10.0+)

Convert property-validator schemas to JSON Schema Draft 7 for OpenAPI compatibility:

import { v, toJsonSchema } from '@tuulbelt/property-validator';

const UserSchema = v.object({
  name: v.string().min(1),
  age: v.number().int().positive(),
  email: v.optional(v.string().email()),
  role: v.union([v.literal('admin'), v.literal('user')])
});

const jsonSchema = toJsonSchema(UserSchema);
// {
//   "$schema": "http://json-schema.org/draft-07/schema#",
//   "type": "object",
//   "properties": {
//     "name": { "type": "string", "minLength": 1 },
//     "age": { "type": "number" },
//     "email": { "type": "string", "format": "email" },
//     "role": { "enum": ["admin", "user"] }
//   },
//   "required": ["name", "age", "role"]
// }

Options:

toJsonSchema(schema, {
  includeSchema: true,     // Include $schema declaration (default: true)
  draft: 'http://json-schema.org/draft-07/schema#',  // JSON Schema draft
  unknownTypeHandling: 'any',  // 'any' | 'throw' | 'empty'
  includeMetadata: false   // Include title/description if available
});

Use Cases:

  • OpenAPI/Swagger — Generate API documentation from your validators
  • Form Generation — Auto-generate forms from schemas
  • Interoperability — Share schemas with other tools/languages
  • Documentation — Self-documenting schemas

Migration from Other Libraries

If you're migrating from another validation library, see MIGRATION.md for a complete guide with side-by-side examples and API comparisons.

Roadmap

Next Up (v1.0.0)

  • Schema generation from existing TypeScript types
  • Async validators for database/API checks
  • Intersection types
  • Streaming validation for large files

Recently Completed

v0.10.0:

  • JSON Schema ExporttoJsonSchema() converts schemas to JSON Schema Draft 7
  • Full Modularization — Validators extracted to individual modules (index.ts: 3744→149 lines)
  • record() validator — Dynamic keys with v.record(keyValidator, valueValidator)
  • discriminatedUnion() — Efficient tagged unions with discriminator field
  • strict() / passthrough() — Control unknown property handling
  • Extended String Validatorscuid(), cuid2(), ulid(), nanoid(), base64(), hex(), jwt()
  • Extended Number Validatorsport(), latitude(), longitude(), percentage()
  • Array JIT for Objects — Optimized array-of-object validation
  • 898 tests — Comprehensive test coverage

v0.9.1:

  • Functional refinement APIstring(email(), minLength(5)) pattern
  • Tree-shakeable refinements — 32 refinement functions as separate exports
  • Backwards compatible — Chainable API still works unchanged
  • 44 new tests — Full test coverage for functional API

v0.9.0:

  • Modular architecture — Tree-shakeable named exports
  • Separate types module@tuulbelt/property-validator/types
  • Package.json exports — Proper bundler support
  • sideEffects: false — Bundle optimization enabled

v0.8.5:

  • check() API — Boolean-only validation (~3x faster than validate)
  • compileCheck() API — Pre-compiled for hot paths
  • ✅ Built-in string validators (email, url, uuid, pattern, etc.)
  • ✅ Built-in number validators (int, positive, negative, range, etc.)
  • ✅ Restructured benchmarks with API equivalence methodology

v0.8.0:

  • ✅ JIT bypass pattern — Direct access to compiled functions
  • ✅ Recursive JIT bypass for nested objects (20x faster)
  • ✅ JIT bypass for arrays, unions, primitives, literals

v0.7.5:

  • ✅ Pre-compiled validators with fast-path optimization
  • ✅ Lazy path building (paths computed only on errors)
  • ✅ tatami-ng benchmarking with criterion-equivalent rigor

v0.4.0:

  • ✅ Schema compilation (v.compile()) with automatic caching
  • ✅ Error formatting (.format('json'), .format('text'), .format('color'))
  • ✅ Circular reference detection (v.lazy())
  • ✅ Security limits (maxDepth, maxProperties, maxItems)

Demo

Demo

▶ View interactive recording on asciinema.org

Try it online: Open in StackBlitz

Demos are automatically generated and embedded via GitHub Actions when demo scripts are updated.

License

MIT — see LICENSE

Contributing

See CONTRIBUTING.md for contribution guidelines.

Related Tools

Part of the Tuulbelt collection:

Technical Notes

Architecture

Property Validator uses a multi-tier optimization strategy:

  1. Schema Compilation — Validators compile to optimized functions at definition time
  2. JIT Fast Pathcheck() and compileCheck() bypass error handling entirely
  3. Lazy Error Paths — Error paths only computed when validation fails

Design Trade-offs

Property Validator prioritizes:

  • Detailed Error Paths — Full paths like users[2].metadata.tags[0]
  • Circular Reference Detection — Prevents infinite loops in recursive schemas
  • Security Limits — DoS protection via maxDepth, maxProperties, maxItems
  • Zero Dependencies — Uses only Node.js standard library

These features add overhead compared to minimal validators, which is why we offer three API tiers:

Need Use
Error messages for users validate()
Fast pass/fail checks check()
Maximum throughput compileCheck()

Running Benchmarks

cd benchmarks
npm install
npm run bench          # Internal API comparison
npm run bench:compare  # Full comparison including competitors

See benchmarks/README.md for methodology and results.

About

Runtime validation for component props

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •