Schema Validator IILoading saved progress…

Schema Validator II

Build the next layer of a tiny schema-validation library — the kind of refinement Zod and Yup are built on. The base library checked types; this one adds chainable range rules. Each builder — s.string(), s.number(), s.array() — returns a validator that exposes .min(n) and .max(n), and each of those returns the validator so the calls chain: s.string().min(3).max(10), s.number().min(0).max(100). For strings and arrays, min/max constrain length; for numbers, they constrain the value itself.

Signature

// A validator carries chainable refinements and one terminal check:
//   type Result    = { valid: boolean; errors: string[] };  // errors empty ⇔ valid
//   type Validator = {
//     min(n: number): Validator;   // returns itself, so calls chain
//     max(n: number): Validator;   // returns itself
//     validate(value: unknown): Result;
//   };

const s = schemaValidator; // the exported namespace of builders

s.string(): Validator   // valid when typeof value === 'string'; min/max bound LENGTH
s.number(): Validator   // valid when value is a finite number; min/max bound VALUE
s.array():  Validator   // valid when Array.isArray(value); min/max bound LENGTH

// Bounds are INCLUSIVE. The TYPE check runs first — if it fails, you get a single
// type error and the range checks never run.

Examples

const s = schemaValidator;

// Strings: min/max constrain length. The bound is inclusive.
s.string().min(3).validate('hi');
// → { valid: false, errors: ['expected length >= 3, got 2'] }
s.string().min(3).max(10).validate('hello');
// → { valid: true, errors: [] }
const s = schemaValidator;

// Numbers: min/max constrain the value. Type is checked FIRST.
s.number().min(0).max(100).validate(150);
// → { valid: false, errors: ['expected value <= 100, got 150'] }
s.number().min(0).validate('99');
// → { valid: false, errors: ['expected number'] }  (fails on type, not range)

Notes

  • .min and .max must return the validator. That's what makes them chain. s.number().min(0).max(100) only works if .min(0) hands back the same validator that .max(100) then refines. Returning the bound, or a fresh object, breaks the chain.
  • Type is checked first, and it short-circuits. A number passed to s.string() fails with 'expected string' — never with a length error. A non-string has no length to measure, so the range rules must not run once the type check has failed.
  • Bounds are inclusive. s.string().min(3).validate('abc') is valid (length 3 meets min 3); s.number().max(100).validate(100) is valid. Test the exact boundary — off-by-one here is the most common bug.
  • Chaining order doesn't matter. .min(3).max(10) and .max(10).min(3) validate the same input identically. Each call just records one bound; order of recording is irrelevant.
  • A bound is optional. A validator with only .min(3) and no .max checks the floor and nothing else. A bare s.number() with no bounds checks only the type.
  • Scope: chainable min/max on primitives. No flat-object combinator (s.object({...}) is the base schema-validator) and no nested / optional support (that's schema-validator-iii). Center the chaining and the bounds.
Loading editor…