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.
// 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.
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)
.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.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.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..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..min(3) and no .max checks the floor and nothing else. A bare s.number() with no bounds checks only the type.s.object({...}) is the base schema-validator) and no nested / optional support (that's schema-validator-iii). Center the chaining and the bounds.The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
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.
// 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.
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)
.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.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.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..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..min(3) and no .max checks the floor and nothing else. A bare s.number() with no bounds checks only the type.s.object({...}) is the base schema-validator) and no nested / optional support (that's schema-validator-iii). Center the chaining and the bounds.The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.