@cleverbrush/schema
Immutable, composable schema definitions with built-in validation and TypeScript type inference.
Bundle size
Measured with esbuild (minified + gzip level 9, single-file bundle, browser target).
| Import | Gzipped | Brotli |
|---|
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
- Define a schema using builder functions like
object(),string(),number()— chain constraints with a fluent API - Infer the TypeScript type with
type T = InferType<typeof MySchema>— no manual interface needed - Validate data with
schema.validate(data)— get typed results with per-property errors (orschema.validateAsync(data)for async validators) - Compose and extend — every method returns a new immutable instance, so you can safely build schema libraries from shared fragments
- Integrate — pass schemas to
@cleverbrush/mapperfor object mapping or@cleverbrush/react-formfor React forms — same schema, everywhere
Quick Start
▶ Open in PlaygroundDefine 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']