Back to Home

PropertyDescriptors for TypeScript schema validation

Validation library uses PropertyDescriptors for type-safe error selectors instead of string paths. Supports immutable fluent API, runtime introspection and extensions. Full compatibility with Standard Schema v1 for the ecosystem.

Type-safe validation: PropertyDescriptors instead of strings
Advertisement 728x90

Property Descriptors for Schema Validation: Type-Safe Error Handling in TypeScript

Developers often grapple with validating complex nested objects in JavaScript projects. Traditional approaches using string paths to extract errors lead to runtime bugs during refactoring. The solution? Property Descriptors: a structured tree of property descriptors that provides type-safe selectors with full IDE autocomplete.

The core idea is that each schema builds a tree of nodes, where every descriptor knows its property path, linked schema, parent, and access methods. This lets you compile selectors via lambdas instead of fragile strings.

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

const OrderSchema = object({
    id: string(),
    customer: object({
        name: string().minLength(2),
        email: string().email(),
        address: object({
            city: string().required('City is required'),
            zip: number()
        })
    }),
    total: number().min(0)
});

const result = OrderSchema.validate({
    id: '123',
    customer: {
        name: 'A',
        email: 'not-an-email',
        address: {
            zip: -1
        }
    },
    total: -5
});

const nameErrors = result.getErrorsFor(t => t.customer.name);
const cityErrors = result.getErrorsFor(t => t.customer.address.city);

Rename a field, and TypeScript catches it at compile time. Your IDE provides full autocomplete.

Google AdInline article slot

Immutable Fluent API and Builders

All schema method calls return new instances, preventing mutations. This makes composition and reuse a breeze.

The library offers 14 builder types:

  • string() — minLength, maxLength, matches, email, url, uuid
  • number() — min, max, positive, negative, finite, multipleOf
  • boolean() — basic validation
  • date() — minDate, maxDate
  • object() — named properties
  • array() — typed elements, nonempty
  • union() — discriminated unions
  • tuple() — fixed length
  • record() — dynamic keys
  • func() — function typing
  • any() — with hasType
  • nul() — null
  • lazy() — recursive schemas
  • extern() — Standard Schema v1 wrapper

Common methods on the base SchemaBuilder:

Google AdInline article slot
  • .optional() — accepts undefined
  • .required('msg') — required field
  • .nullable() — accepts null
  • .default(value) — default value
  • .catch(value) — fallback on error
  • .readonly() — Readonly<T>
  • .brand<'T'>() — branded types
  • .describe('text') — JSDoc preservation
  • .addValidator(fn) — custom logic
  • .addPreprocessor(fn) — preprocessing
  • .introspect() — metadata

Runtime Schema Introspection

The .introspect() method returns a full schema description: type, constraints, flags, descriptions, extensions. All available at runtime for tools and integrations.

const Name = string().minLength(2).maxLength(100).describe('User name').optional();

const info = Name.introspect();
// info.type === 'string'
// info.minLength === 2
// info.description === 'User name'

Introspection is fully expanded for each builder. Perfect for JSON Schema conversion, form generation, or custom renderers.

Typed Extension System

Extensions are composable, introspectable add-ons to the core. Example for HEX colors and ports:

Google AdInline article slot
const hexColorExt = defineExtension({
    string: {
        hexColor(this: StringSchemaBuilder) {
            return this.addValidator((v) => {
                const valid = /^#[0-9a-f]{6}$/i.test(v as string);
                return {
                    valid,
                    errors: valid ? [] : [{ message: 'Must be a valid HEX color' }]
                };
            });
        }
    }
});

const s = withExtensions(hexColorExt, portExt);
const colorSchema = s.string().hexColor();

Extensions appear in .introspect().extensions. Built-ins include email, url, uuid, positive, nonempty.

Standard Schema v1 Compatibility

Schemas implement Standard Schema v1 for seamless integration with tRPC, TanStack Form, React Hook Form, Hono. extern() wraps Zod/Valibot into builders.

const UserSchema = object({
    name: string().minLength(2),
    address: extern(zodAddress)
});

Property Descriptors deliver type-safe error access even in mixed schemas.

Key Takeaways

  • Type-safe selectors via Property Descriptors eliminate string paths and runtime bugs.
  • Immutable fluent API simplifies composition without side effects.
  • Runtime introspection extracts metadata for any tool.
  • Extensions — typed, composable core add-ons.
  • Standard Schema v1 ensures ecosystem compatibility.

— Editorial Team

Advertisement 728x90

Read Next