@cleverbrush/schema

Immutable, composable schema definitions with built-in validation and TypeScript type inference.

$npm install @cleverbrush/schema
Bundle size

Measured with esbuild (minified + gzip level 9, single-file bundle, browser target).

ImportGzippedBrotli

The sideEffects: false flag is set in the package manifest. When your bundler supports tree-shaking, use the sub-path exports above to keep your bundle smaller — each builder carries only its own validation logic plus the shared SchemaBuilder base (~2.7 KB gzip).

💡 Why @cleverbrush/schema?

The Problem

In a typical TypeScript project, types and runtime validation are separate concerns. You define a User type in one file, then write Joi / Yup / Zod schemas (or manual if checks) in another. Over time these drift apart — the type says a field is required, but the validation allows it to be undefined. Tests pass, but production data breaks because the validation didn't match the type.

The Solution

@cleverbrush/schema lets you define a schema once and derive both the TypeScript type (via InferType) and runtime validation from the same source. Because every method returns a new builder instance (immutability), you can safely compose and extend schemas without accidentally mutating shared definitions.

The Unique Feature: PropertyDescriptors

Unlike other schema libraries, @cleverbrush/schema exposes a typed property descriptor for every field in a schema object. This lets you reference fields with a typed arrow function — u => u.address.city — instead of a string literal like "address.city". TypeScript verifies the path at compile time, so renaming a field immediately surfaces every stale reference as a compile error. No silent runtime breakage from string paths that drift out of sync. The @cleverbrush/mapper and @cleverbrush/react-form packages both build on this for type-safe field targeting.

Production Tested

Every form, every API response mapping, and every validation rule in cleverbrush.com/editor is powered by @cleverbrush/schema. It handles hundreds of schemas with nested objects, async validators, and custom error messages in production every day.

How It Works — Step by Step

  1. Define a schema using builder functions like object(), string(), number() — chain constraints with a fluent API
  2. Infer the TypeScript type with type T = InferType<typeof MySchema> — no manual interface needed
  3. Validate data with schema.validate(data) — get typed results with per-property errors (or schema.validateAsync(data) for async validators)
  4. Compose and extend — every method returns a new immutable instance, so you can safely build schema libraries from shared fragments
  5. Integrate — pass schemas to @cleverbrush/mapper for object mapping or @cleverbrush/react-form for React forms — same schema, everywhere

Quick Start

▶ Open in Playground

Define a schema, infer its TypeScript type, and validate data — all from a single definition:

import { object, string, number, boolean, type InferType } from '@cleverbrush/schema';

// Define a schema with fluent constraints and custom error messages
const UserSchema = object({
  name:     string().nonempty('Name is required').minLength(2, 'Name must be at least 2 characters'),
  email:    string().email('Please enter a valid email address'),
  age:      number().min(0, 'Age cannot be negative').max(150, 'Age seems unrealistic').positive(),
  isActive: boolean()
});

// TypeScript type is inferred automatically — no duplication!
type User = InferType<typeof UserSchema>;
// Equivalent to: { name: string; email: string; age: number; isActive: boolean }

// Validate data at runtime — synchronous by default
const result = UserSchema.validate({
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
  isActive: true
});

console.log(result.valid);  // true
console.log(result.object); // the validated object

// Invalid data produces structured errors
const bad = UserSchema.validate(
  { name: 'A', email: '', age: -5, isActive: true },
  { doNotStopOnFirstError: true }
);

console.log(bad.valid);  // false

// Use getErrorsFor() to inspect per-field errors
console.log(bad.getErrorsFor(u => u.name).errors);   // ['Name must be at least 2 characters']
console.log(bad.getErrorsFor(u => u.email).errors);  // ['Please enter a valid email address']
console.log(bad.getErrorsFor(u => u.age).errors);    // ['Age cannot be negative']