Generic Schemas

▶ Open in Playground

Use generic(fn) to create reusable, parameterized schema templates. The template function accepts one or more schema builders as arguments and returns a concrete schema. Call .apply(...schemas)to instantiate the template with specific schemas — TypeScript infers the resulting type automatically from the function's own generic signature.

Single type parameter — a paginated list that works for any element type:

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

const PaginatedList = generic(
    <T extends SchemaBuilder<any, any, any, any, any>>(itemSchema: T) =>
        object({
            items: array(itemSchema),
            total: number(),
            page:  number(),
        })
);

// Instantiate with a concrete schema — TypeScript infers the full type
const PaginatedUsers = PaginatedList.apply(
    object({ name: string(), age: number() })
);

type PaginatedUsersType = InferType<typeof PaginatedUsers>;
// → { items: { name: string; age: number }[]; total: number; page: number }

PaginatedUsers.validate({
    items: [{ name: 'Alice', age: 30 }],
    total: 1,
    page:  1
});
// { valid: true, object: { items: [...], total: 1, page: 1 } }

Multiple type parameters — a Result / Either type with independent value and error schemas:

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

const Result = generic(
    <
        T extends SchemaBuilder<any, any, any, any, any>,
        E extends SchemaBuilder<any, any, any, any, any>
    >(
        valueSchema: T,
        errorSchema: E
    ) =>
        object({
            ok:    boolean(),
            value: valueSchema.optional(),
            error: errorSchema.optional(),
        })
);

const StringResult = Result.apply(string(), number());

type StringResultType = InferType<typeof StringResult>;
// → { ok: boolean; value?: string; error?: number }

StringResult.validate({ ok: true,  value: 'hello' }); // valid
StringResult.validate({ ok: false, error: 404 });      // valid

Default arguments — supply a defaults array as the first argument so the template can be validated directly without calling .apply() first:

import {
    generic, object, array, number, any,
    type SchemaBuilder
} from '@cleverbrush/schema';

// 'any()' is the default for the single type parameter
const AnyList = generic(
    [any()],
    <T extends SchemaBuilder<any, any, any, any, any>>(itemSchema: T) =>
        object({ items: array(itemSchema), total: number() })
);

// Validate directly — uses the 'any()' default
AnyList.validate({ items: [1, 'two', true], total: 3 }); // valid

// Or apply concrete schemas first for a stricter type
AnyList.apply(string()).validate({ items: ['a', 'b'], total: 2 }); // valid
Tip: Each call to .apply() returns an independent schema builder. You can call .optional(), .addValidator(), .default(value), and every other fluent method on the result just like you would on any other schema.